From 566a58b5d82fd25b872a2381d07c383ae7e2738d Mon Sep 17 00:00:00 2001 From: Thomas Raehalme Date: Tue, 15 Dec 2015 11:53:10 +0200 Subject: [PATCH 01/65] Replaced AdapterDeploymentContextBean with AdapterDeploymentContextFactoryBean and added support for KeycloakConfigResolver. --- .../AdapterDeploymentContextBean.java | 64 --------------- .../AdapterDeploymentContextFactoryBean.java | 79 +++++++++++++++++++ .../authentication/KeycloakLogoutHandler.java | 15 ++-- .../KeycloakWebSecurityConfigurerAdapter.java | 23 ++++-- ...eycloakAuthenticationProcessingFilter.java | 11 +-- .../filter/KeycloakPreAuthActionsFilter.java | 5 +- .../AdapterDeploymentContextBeanTest.java | 56 ------------- ...apterDeploymentContextFactoryBeanTest.java | 77 ++++++++++++++++++ .../KeycloakLogoutHandlerTest.java | 9 ++- ...oakAuthenticationProcessingFilterTest.java | 9 ++- 10 files changed, 200 insertions(+), 148 deletions(-) delete mode 100644 integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java create mode 100644 integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java delete mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBeanTest.java create mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java deleted file mode 100644 index a517416396..0000000000 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBean.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.keycloak.adapters.springsecurity; - -import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.io.Resource; - -import java.io.FileNotFoundException; -import java.io.IOException; - -/** - * Bean holding the {@link KeycloakDeployment} and {@link AdapterDeploymentContext} for this - * Spring application context. The Keycloak deployment is loaded from the required - * keycloak.json resource file. - * - * @author Scott Rossillo - * @version $Revision: 1 $ - */ -public class AdapterDeploymentContextBean implements InitializingBean { - - private final Resource keycloakConfigFileResource; - - private AdapterDeploymentContext deploymentContext; - private KeycloakDeployment deployment; - - public AdapterDeploymentContextBean(Resource keycloakConfigFileResource) { - this.keycloakConfigFileResource = keycloakConfigFileResource; - } - - @Override - public void afterPropertiesSet() throws Exception { - this.deployment = loadKeycloakDeployment(); - this.deploymentContext = new AdapterDeploymentContext(deployment); - } - - private KeycloakDeployment loadKeycloakDeployment() throws IOException { - - if (!keycloakConfigFileResource.isReadable()) { - throw new FileNotFoundException(String.format("Unable to locate Keycloak configuration file: %s", - keycloakConfigFileResource.getFilename())); - } - - return KeycloakDeploymentBuilder.build(keycloakConfigFileResource.getInputStream()); - } - - /** - * Returns the Keycloak {@link AdapterDeploymentContext} for this application context. - * - * @return the Keycloak {@link AdapterDeploymentContext} for this application context - */ - public AdapterDeploymentContext getDeploymentContext() { - return deploymentContext; - } - - /** - * Returns the {@link KeycloakDeployment} for this application context. - * - * @return the {@link KeycloakDeployment} for this application context - */ - public KeycloakDeployment getDeployment() { - return deployment; - } -} diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java new file mode 100644 index 0000000000..d089ded86a --- /dev/null +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBean.java @@ -0,0 +1,79 @@ +package org.keycloak.adapters.springsecurity; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Objects; + +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; + +/** + * {@link FactoryBean} that creates an {@link AdapterDeploymentContext} given a {@link Resource} defining the Keycloak + * client configuration or a {@link KeycloakConfigResolver} for multi-tenant environments. + * + * @author Thomas Raehalme + */ +public class AdapterDeploymentContextFactoryBean + implements FactoryBean, InitializingBean { + private static final Logger log = + LoggerFactory.getLogger(AdapterDeploymentContextFactoryBean.class); + private final Resource keycloakConfigFileResource; + private final KeycloakConfigResolver keycloakConfigResolver; + private AdapterDeploymentContext adapterDeploymentContext; + + public AdapterDeploymentContextFactoryBean(Resource keycloakConfigFileResource) { + this.keycloakConfigFileResource = Objects.requireNonNull(keycloakConfigFileResource); + this.keycloakConfigResolver = null; + } + + public AdapterDeploymentContextFactoryBean(KeycloakConfigResolver keycloakConfigResolver) { + this.keycloakConfigResolver = Objects.requireNonNull(keycloakConfigResolver); + this.keycloakConfigFileResource = null; + } + + @Override + public Class getObjectType() { + return AdapterDeploymentContext.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (keycloakConfigResolver != null) { + adapterDeploymentContext = new AdapterDeploymentContext(keycloakConfigResolver); + } + else { + log.info("Loading Keycloak deployment from configuration file: {}", keycloakConfigFileResource); + + KeycloakDeployment deployment = loadKeycloakDeployment(); + adapterDeploymentContext = new AdapterDeploymentContext(deployment); + } + } + + private KeycloakDeployment loadKeycloakDeployment() throws IOException { + if (!keycloakConfigFileResource.isReadable()) { + throw new FileNotFoundException(String.format("Unable to locate Keycloak configuration file: %s", + keycloakConfigFileResource.getFilename())); + } + + return KeycloakDeploymentBuilder.build(keycloakConfigFileResource.getInputStream()); + } + + @Override + public AdapterDeploymentContext getObject() throws Exception { + return adapterDeploymentContext; + } +} diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java index 27178ca2ad..c17dca1706 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java @@ -1,8 +1,10 @@ package org.keycloak.adapters.springsecurity.authentication; +import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; +import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,11 +25,11 @@ public class KeycloakLogoutHandler implements LogoutHandler { private static final Logger log = LoggerFactory.getLogger(KeycloakLogoutHandler.class); - private AdapterDeploymentContextBean deploymentContextBean; + private AdapterDeploymentContext adapterDeploymentContext; - public KeycloakLogoutHandler(AdapterDeploymentContextBean deploymentContextBean) { - Assert.notNull(deploymentContextBean); - this.deploymentContextBean = deploymentContextBean; + public KeycloakLogoutHandler(AdapterDeploymentContext adapterDeploymentContext) { + Assert.notNull(adapterDeploymentContext); + this.adapterDeploymentContext = adapterDeploymentContext; } @Override @@ -45,7 +47,8 @@ public class KeycloakLogoutHandler implements LogoutHandler { } protected void handleSingleSignOut(HttpServletRequest request, HttpServletResponse response, KeycloakAuthenticationToken authenticationToken) { - KeycloakDeployment deployment = deploymentContextBean.getDeployment(); + HttpFacade facade = new SimpleHttpFacade(request, response); + KeycloakDeployment deployment = adapterDeploymentContext.resolveDeployment(facade); RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) authenticationToken.getAccount().getKeycloakSecurityContext(); session.logout(deployment); } diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java index b5ef6659e3..55c6b3c700 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java @@ -1,6 +1,8 @@ package org.keycloak.adapters.springsecurity.config; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.springsecurity.AdapterDeploymentContextFactoryBean; import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; import org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler; @@ -8,6 +10,7 @@ import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcess import org.keycloak.adapters.springsecurity.filter.KeycloakCsrfRequestMatcher; import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter; import org.keycloak.adapters.springsecurity.management.HttpSessionManager; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.core.io.Resource; @@ -35,10 +38,20 @@ public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityCo @Value("${keycloak.configurationFile:WEB-INF/keycloak.json}") private Resource keycloakConfigFileResource; + @Autowired(required = false) + private KeycloakConfigResolver keycloakConfigResolver; @Bean - protected AdapterDeploymentContextBean adapterDeploymentContextBean() { - return new AdapterDeploymentContextBean(keycloakConfigFileResource); + protected AdapterDeploymentContext adapterDeploymentContext() throws Exception { + AdapterDeploymentContextFactoryBean factoryBean; + if (keycloakConfigResolver != null) { + factoryBean = new AdapterDeploymentContextFactoryBean(keycloakConfigResolver); + } + else { + factoryBean = new AdapterDeploymentContextFactoryBean(keycloakConfigFileResource); + } + factoryBean.afterPropertiesSet(); + return factoryBean.getObject(); } protected AuthenticationEntryPoint authenticationEntryPoint() { @@ -70,8 +83,8 @@ public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityCo return new HttpSessionManager(); } - protected KeycloakLogoutHandler keycloakLogoutHandler() { - return new KeycloakLogoutHandler(adapterDeploymentContextBean()); + protected KeycloakLogoutHandler keycloakLogoutHandler() throws Exception { + return new KeycloakLogoutHandler(adapterDeploymentContext()); } protected abstract SessionAuthenticationStrategy sessionAuthenticationStrategy(); diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java index 965c1624ab..04c6ed3367 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java @@ -1,11 +1,12 @@ package org.keycloak.adapters.springsecurity.filter; +import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; +import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException; import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator; @@ -56,7 +57,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class); private ApplicationContext applicationContext; - private AdapterDeploymentContextBean adapterDeploymentContextBean; + private AdapterDeploymentContext adapterDeploymentContext; private AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory(); private AuthenticationManager authenticationManager; @@ -100,7 +101,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati @Override public void afterPropertiesSet() { - adapterDeploymentContextBean = applicationContext.getBean(AdapterDeploymentContextBean.class); + adapterDeploymentContext = applicationContext.getBean(AdapterDeploymentContext.class); super.afterPropertiesSet(); } @@ -110,8 +111,8 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati log.debug("Attempting Keycloak authentication"); - KeycloakDeployment deployment = adapterDeploymentContextBean.getDeployment(); - SimpleHttpFacade facade = new SimpleHttpFacade(request, response); + HttpFacade facade = new SimpleHttpFacade(request, response); + KeycloakDeployment deployment = adapterDeploymentContext.resolveDeployment(facade); AdapterTokenStore tokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, request); RequestAuthenticator authenticator = new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, -1); diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java index 2363b3f1f3..565ae624ba 100755 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakPreAuthActionsFilter.java @@ -5,7 +5,6 @@ import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.spi.UserSessionManagement; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,9 +46,7 @@ public class KeycloakPreAuthActionsFilter extends GenericFilterBean implements A @Override protected void initFilterBean() throws ServletException { - AdapterDeploymentContextBean contextBean = applicationContext.getBean(AdapterDeploymentContextBean.class); - deploymentContext = contextBean.getDeploymentContext(); - management.tryRegister(contextBean.getDeployment()); + deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class); } @Override diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBeanTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBeanTest.java deleted file mode 100644 index 3510db7f7a..0000000000 --- a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextBeanTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.keycloak.adapters.springsecurity; - - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; - -import java.io.FileNotFoundException; - -import static org.junit.Assert.assertNotNull; - -public class AdapterDeploymentContextBeanTest { - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private AdapterDeploymentContextBean adapterDeploymentContextBean; - - @Test - public void should_create_deployment_and_deployment_context() throws Exception { - - //given: - adapterDeploymentContextBean = new AdapterDeploymentContextBean(getCorrectResource()); - - //when: - adapterDeploymentContextBean.afterPropertiesSet(); - - //then - assertNotNull(adapterDeploymentContextBean.getDeployment()); - assertNotNull(adapterDeploymentContextBean.getDeploymentContext()); - } - - private Resource getCorrectResource() { - return new ClassPathResource("keycloak.json"); - } - - @Test - public void should_throw_exception_when_configuration_file_was_not_found() throws Exception { - - //given: - adapterDeploymentContextBean = new AdapterDeploymentContextBean(getEmptyResource()); - - //then: - expectedException.expect(FileNotFoundException.class); - expectedException.expectMessage("Unable to locate Keycloak configuration file: no-file.json"); - - //when: - adapterDeploymentContextBean.afterPropertiesSet(); - } - - private Resource getEmptyResource() { - return new ClassPathResource("no-file.json"); - } -} diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java new file mode 100644 index 0000000000..6a3b39fd37 --- /dev/null +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/AdapterDeploymentContextFactoryBeanTest.java @@ -0,0 +1,77 @@ +package org.keycloak.adapters.springsecurity; + +import java.io.FileNotFoundException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.spi.HttpFacade; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.junit.Assert.assertNotNull; + +public class AdapterDeploymentContextFactoryBeanTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private AdapterDeploymentContextFactoryBean adapterDeploymentContextFactoryBean; + + @Test + public void should_create_adapter_deployment_context_from_configuration_file() throws Exception { + // given: + adapterDeploymentContextFactoryBean = new AdapterDeploymentContextFactoryBean(getCorrectResource()); + + // when: + adapterDeploymentContextFactoryBean.afterPropertiesSet(); + + // then + assertNotNull(adapterDeploymentContextFactoryBean.getObject()); + } + + private Resource getCorrectResource() { + return new ClassPathResource("keycloak.json"); + } + + @Test + public void should_throw_exception_when_configuration_file_was_not_found() throws Exception { + // given: + adapterDeploymentContextFactoryBean = new AdapterDeploymentContextFactoryBean(getEmptyResource()); + + // then: + expectedException.expect(FileNotFoundException.class); + expectedException.expectMessage("Unable to locate Keycloak configuration file: no-file.json"); + + // when: + adapterDeploymentContextFactoryBean.afterPropertiesSet(); + } + + private Resource getEmptyResource() { + return new ClassPathResource("no-file.json"); + } + + @Test + public void should_create_adapter_deployment_context_from_keycloak_config_resolver() throws Exception { + // given: + adapterDeploymentContextFactoryBean = new AdapterDeploymentContextFactoryBean(getKeycloakConfigResolver()); + + // when: + adapterDeploymentContextFactoryBean.afterPropertiesSet(); + + // then: + assertNotNull(adapterDeploymentContextFactoryBean.getObject()); + } + + private KeycloakConfigResolver getKeycloakConfigResolver() { + return new KeycloakConfigResolver() { + @Override + public KeycloakDeployment resolve(HttpFacade.Request facade) { + return null; + } + }; + } +} diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java index cc751c4b76..2f44107ba6 100755 --- a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandlerTest.java @@ -2,10 +2,11 @@ package org.keycloak.adapters.springsecurity.authentication; import org.junit.Before; import org.junit.Test; +import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OidcKeycloakAccount; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; +import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.springsecurity.account.KeycloakRole; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.mockito.Mock; @@ -35,7 +36,7 @@ public class KeycloakLogoutHandlerTest { private MockHttpServletResponse response; @Mock - private AdapterDeploymentContextBean adapterDeploymentContextBean; + private AdapterDeploymentContext adapterDeploymentContext; @Mock private OidcKeycloakAccount keycloakAccount; @@ -52,11 +53,11 @@ public class KeycloakLogoutHandlerTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); keycloakAuthenticationToken = mock(KeycloakAuthenticationToken.class); - keycloakLogoutHandler = new KeycloakLogoutHandler(adapterDeploymentContextBean); + keycloakLogoutHandler = new KeycloakLogoutHandler(adapterDeploymentContext); request = new MockHttpServletRequest(); response = new MockHttpServletResponse(); - when(adapterDeploymentContextBean.getDeployment()).thenReturn(keycloakDeployment); + when(adapterDeploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(keycloakDeployment); when(keycloakAuthenticationToken.getAccount()).thenReturn(keycloakAccount); when(keycloakAccount.getKeycloakSecurityContext()).thenReturn(session); } diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java index ab4c03258f..1ccc3671b5 100755 --- a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java @@ -4,9 +4,10 @@ import org.junit.Before; import org.junit.Test; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; +import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException; import org.keycloak.adapters.springsecurity.account.KeycloakRole; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; @@ -45,7 +46,7 @@ public class KeycloakAuthenticationProcessingFilterTest { private AuthenticationManager authenticationManager; @Mock - private AdapterDeploymentContextBean adapterDeploymentContextBean; + private AdapterDeploymentContext adapterDeploymentContext; @Mock private FilterChain chain; @@ -85,8 +86,8 @@ public class KeycloakAuthenticationProcessingFilterTest { filter.setAuthenticationSuccessHandler(successHandler); filter.setAuthenticationFailureHandler(failureHandler); - when(applicationContext.getBean(eq(AdapterDeploymentContextBean.class))).thenReturn(adapterDeploymentContextBean); - when(adapterDeploymentContextBean.getDeployment()).thenReturn(keycloakDeployment); + when(applicationContext.getBean(eq(AdapterDeploymentContext.class))).thenReturn(adapterDeploymentContext); + when(adapterDeploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(keycloakDeployment); when(keycloakAccount.getPrincipal()).thenReturn( new KeycloakPrincipal(UUID.randomUUID().toString(), keycloakSecurityContext)); From b4e53b16777dc6f439e4309ff8dd1d6e4a8d8afd Mon Sep 17 00:00:00 2001 From: Pavel Drozd Date: Wed, 16 Dec 2015 10:26:10 +0100 Subject: [PATCH 02/65] Arquillian xsl templates - changed namespaces to WF10. --- .../servers/wildfly/src/main/xslt/datasource.xsl | 4 ++-- .../servers/wildfly/src/main/xslt/security.xsl | 6 +++--- .../servers/wildfly/src/main/xslt/standalone.xsl | 4 ++-- .../tests/adapters/wildfly/src/main/xslt/security.xsl | 6 +++--- .../tests/adapters/wildfly/src/main/xslt/standalone.xsl | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/datasource.xsl b/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/datasource.xsl index c06899fd74..0c6b3e241a 100644 --- a/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/datasource.xsl +++ b/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/datasource.xsl @@ -1,7 +1,7 @@ diff --git a/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/standalone.xsl b/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/standalone.xsl index 9239d673e5..f711ed93b3 100644 --- a/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/standalone.xsl +++ b/testsuite/integration-arquillian/servers/wildfly/src/main/xslt/standalone.xsl @@ -1,7 +1,7 @@ diff --git a/testsuite/integration-arquillian/tests/adapters/wildfly/src/main/xslt/standalone.xsl b/testsuite/integration-arquillian/tests/adapters/wildfly/src/main/xslt/standalone.xsl index a48371789d..c9d71ed5b2 100644 --- a/testsuite/integration-arquillian/tests/adapters/wildfly/src/main/xslt/standalone.xsl +++ b/testsuite/integration-arquillian/tests/adapters/wildfly/src/main/xslt/standalone.xsl @@ -1,7 +1,7 @@ Date: Mon, 14 Dec 2015 16:29:01 +0100 Subject: [PATCH 03/65] Minor test fix --- .../federation/FederationProvidersIntegrationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java index ef1a721d2d..7a8a01ab0d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java @@ -684,10 +684,11 @@ public class FederationProvidersIntegrationTest { session.users().searchForUser("user5@email.org", appRealm); FederationTestUtils.assertUserImported(session.userStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125"); - session.users().searchForUser("user6@email.org", appRealm); + session.users().searchForUser("John6 Doel6", appRealm); FederationTestUtils.assertUserImported(session.userStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126"); session.users().searchForUser("user7@email.org", appRealm); + session.users().searchForUser("John7 Doel7", appRealm); Assert.assertNull(session.userStorage().getUserByUsername("username7", appRealm)); // Remove custom filter From 215d59b1e5d213e4e468698ad2b35e3af763b70f Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 14 Dec 2015 16:30:23 +0100 Subject: [PATCH 04/65] KEYCLOAK-2053 Memberships based on memberUid like attribute --- .../mappers/RoleLDAPFederationMapper.java | 63 +++++++++++++++---- .../RoleLDAPFederationMapperFactory.java | 20 ++++-- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java index 6c45481a99..a62cc5ec24 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -39,11 +39,14 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { // Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn" public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute"; + // Object classes of the role object. + public static final String ROLE_OBJECT_CLASSES = "role.object.classes"; + // Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member" public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute"; - // Object classes of the role object. - public static final String ROLE_OBJECT_CLASSES = "role.object.classes"; + // See docs for MembershipType enum + public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type"; // Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID) public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping"; @@ -179,6 +182,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER; } + protected MembershipType getMembershipTypeLdapAttribute(UserFederationMapperModel mapperModel) { + String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE); + return membershipType!=null ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; + } + + private String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) { + return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName()); + } + protected Collection getRoleObjectClasses(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES); if (objectClasses == null) { @@ -228,17 +240,23 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider); } + MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel); + Set memberships = getExistingMemberships(mapperModel, ldapRole); // Remove membership placeholder if present - for (String membership : memberships) { - if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) { - memberships.remove(membership); - break; + if (membershipType == MembershipType.DN) { + for (String membership : memberships) { + if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) { + memberships.remove(membership); + break; + } } } - memberships.add(ldapUser.getDn().toString()); + String membership = getMembershipFromUser(ldapUser, membershipType); + + memberships.add(membership); ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships); ldapProvider.getLdapIdentityStore().update(ldapRole); @@ -246,10 +264,14 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) { Set memberships = getExistingMemberships(mapperModel, ldapRole); - memberships.remove(ldapUser.getDn().toString()); + + MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel); + String userMembership = getMembershipFromUser(ldapUser, membershipType); + + memberships.remove(userMembership); // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here) - if (memberships.size() == 0 && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) { + if (memberships.size() == 0 && membershipType==MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) { memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE); } @@ -276,7 +298,10 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { protected List getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); String membershipAttr = getMembershipLdapAttribute(mapperModel); - Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, ldapUser.getDn().toString()); + + String userMembership = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel)); + + Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); ldapQuery.addWhereCondition(membershipCondition); return ldapQuery.getResultList(); } @@ -437,7 +462,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); Condition roleNameCondition = conditionsBuilder.equal(getRoleNameLdapAttribute(mapperModel), role.getName()); - Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), ldapUser.getDn().toString()); + String membershipUserAttr = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel)); + Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), membershipUserAttr); ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition); LDAPObject ldapRole = ldapQuery.getFirstResult(); @@ -462,6 +488,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { } } + public enum Mode { /** * All role mappings are retrieved from LDAP and saved into LDAP @@ -484,4 +511,18 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { */ READ_ONLY } + + + public enum MembershipType { + + /** + * Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" ) + */ + DN, + + /** + * Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" ) + */ + UID + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java index bdd0e51e63..b244d2286e 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java @@ -42,15 +42,25 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN); configProperties.add(roleNameLDAPAttribute); + ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes", + "Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ", + ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(roleObjectClasses); + ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute", "Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ", ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER); configProperties.add(membershipLDAPAttribute); - ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes", - "Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ", - ProviderConfigProperty.STRING_TYPE, null); - configProperties.add(roleObjectClasses); + List membershipTypes = new LinkedList<>(); + for (RoleLDAPFederationMapper.MembershipType membershipType : RoleLDAPFederationMapper.MembershipType.values()) { + membershipTypes.add(membershipType.toString()); + } + ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", + "DN means that LDAP role has it's members declared in form of their full DN. For example ( 'member: uid=john,ou=users,dc=example,dc=com' . " + + "UID means that LDAP role has it's members declared in form of pure user uids. For example ( 'memberuid: john' ))", + ProviderConfigProperty.LIST_TYPE, membershipTypes); + configProperties.add(membershipType); ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER, "LDAP Filter", @@ -58,7 +68,7 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty.STRING_TYPE, null); configProperties.add(ldapFilter); - List modes = new LinkedList(); + List modes = new LinkedList<>(); for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) { modes.add(mode.toString()); } From 358c273d39c0211dfbfacf611e52c0736d443686 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 14 Dec 2015 23:21:11 +0100 Subject: [PATCH 05/65] KEYCLOAK-2227 Added UserRolesRetrieveStrategy. Possibility to read user role mappings through 'memberOf' attribute --- .../federation/ldap/idm/model/LDAPDn.java | 23 ++++ .../mappers/RoleLDAPFederationMapper.java | 24 ++-- .../RoleLDAPFederationMapperFactory.java | 21 ++- .../mappers/UserRolesRetrieveStrategy.java | 124 ++++++++++++++++++ .../federation/ldap/idm/model/LDAPDnTest.java | 9 ++ .../org/keycloak/models/LDAPConstants.java | 2 + 6 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java index a7cf098702..be5e6b9d94 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java @@ -62,6 +62,14 @@ public class LDAPDn { return firstEntry.attrName; } + /** + * @return string attribute value like "joe" from the DN like "uid=joe,dc=something,dc=org" + */ + public String getFirstRdnAttrValue() { + Entry firstEntry = entries.getFirst(); + return firstEntry.attrValue; + } + /** * * @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org" @@ -72,6 +80,21 @@ public class LDAPDn { return toString(parentDnEntries); } + public boolean isDescendantOf(LDAPDn expectedParentDn) { + int parentEntriesCount = expectedParentDn.entries.size(); + + Deque myEntries = new LinkedList<>(this.entries); + boolean someRemoved = false; + while (myEntries.size() > parentEntriesCount) { + myEntries.removeFirst(); + someRemoved = true; + } + + String myEntriesParentStr = toString(myEntries).toLowerCase(); + String expectedParentDnStr = expectedParentDn.toString().toLowerCase(); + return someRemoved && myEntriesParentStr.equals(expectedParentDnStr); + } + public void addFirst(String rdnName, String rdnValue) { rdnValue = escape(rdnValue); entries.addFirst(new Entry(rdnName, rdnValue)); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java index a62cc5ec24..7eac273ca8 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -56,6 +56,9 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { // See docs for Mode enum public static final String MODE = "mode"; + + // See docs for UserRolesRetriever enum + public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy"; // Customized LDAP filter which is added to the whole LDAP query public static final String ROLES_LDAP_FILTER = "roles.ldap.filter"; @@ -184,10 +187,10 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { protected MembershipType getMembershipTypeLdapAttribute(UserFederationMapperModel mapperModel) { String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE); - return membershipType!=null ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; + return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; } - private String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) { + protected String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) { return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName()); } @@ -218,6 +221,11 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { return Enum.valueOf(Mode.class, modeString.toUpperCase()); } + private UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(UserFederationMapperModel mapperModel) { + String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY); + return (strategyString!=null && !strategyString.isEmpty()) ? Enum.valueOf(UserRolesRetrieveStrategy.class, strategyString) : UserRolesRetrieveStrategy.LOAD_ROLES_BY_MEMBER_ATTRIBUTE; + } + public LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) { LDAPObject ldapObject = new LDAPObject(); String roleNameAttribute = getRoleNameLdapAttribute(mapperModel); @@ -296,14 +304,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { } protected List getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); - String membershipAttr = getMembershipLdapAttribute(mapperModel); - - String userMembership = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel)); - - Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); - ldapQuery.addWhereCondition(membershipCondition); - return ldapQuery.getResultList(); + UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel); + return strategy.getLDAPRoleMappings(this, mapperModel, ldapProvider, ldapUser); } @Override @@ -320,6 +322,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { @Override public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel); + strategy.beforeUserLDAPQuery(mapperModel, query); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java index b244d2286e..5cc2997a4b 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java @@ -52,22 +52,25 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER); configProperties.add(membershipLDAPAttribute); + List membershipTypes = new LinkedList<>(); for (RoleLDAPFederationMapper.MembershipType membershipType : RoleLDAPFederationMapper.MembershipType.values()) { membershipTypes.add(membershipType.toString()); } ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", "DN means that LDAP role has it's members declared in form of their full DN. For example ( 'member: uid=john,ou=users,dc=example,dc=com' . " + - "UID means that LDAP role has it's members declared in form of pure user uids. For example ( 'memberuid: john' ))", + "UID means that LDAP role has it's members declared in form of pure user uids. For example ( 'memberUid: john' ))", ProviderConfigProperty.LIST_TYPE, membershipTypes); configProperties.add(membershipType); - + + ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER, "LDAP Filter", "LDAP Filter adds additional custom filter to the whole query. Make sure that it starts with '(' and ends with ')'", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(ldapFilter); + List modes = new LinkedList<>(); for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) { modes.add(mode.toString()); @@ -79,6 +82,20 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty.LIST_TYPE, modes); configProperties.add(mode); + + List roleRetrievers = new LinkedList<>(); + for (UserRolesRetrieveStrategy retriever : UserRolesRetrieveStrategy.values()) { + roleRetrievers.add(retriever.toString()); + } + ProviderConfigProperty retriever = createConfigProperty(RoleLDAPFederationMapper.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy", + "Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " + + "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " + + "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN extension." + , + ProviderConfigProperty.LIST_TYPE, roleRetrievers); + configProperties.add(retriever); + + ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping", "If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true"); configProperties.add(useRealmRolesMappings); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java new file mode 100644 index 0000000000..dd7cd317c2 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java @@ -0,0 +1,124 @@ +package org.keycloak.federation.ldap.mappers; + + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.idm.model.LDAPDn; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.UserFederationMapperModel; + +/** + * Strategy for how to retrieve LDAP roles of user + * + * @author Marek Posolda + */ +public enum UserRolesRetrieveStrategy { + + + /** + * Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user + */ + LOAD_ROLES_BY_MEMBER_ATTRIBUTE { + + @Override + public List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { + LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider); + String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel); + + String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel)); + + Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); + ldapQuery.addWhereCondition(membershipCondition); + return ldapQuery.getResultList(); + } + + @Override + public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + } + + }, + + + /** + * Roles of user will be retrieved from "memberOf" attribute of our user + */ + GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE { + + @Override + public List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { + Set memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF); + if (memberOfValues == null) { + return Collections.emptyList(); + } + + List roles = new LinkedList<>(); + LDAPDn parentDn = LDAPDn.fromString(roleMapper.getRolesDn(mapperModel)); + + for (String roleDn : memberOfValues) { + LDAPDn roleDN = LDAPDn.fromString(roleDn); + if (roleDN.isDescendantOf(parentDn)) { + LDAPObject role = new LDAPObject(); + role.setDn(roleDN); + + String firstDN = roleDN.getFirstRdnAttrName(); + if (firstDN.equalsIgnoreCase(roleMapper.getRoleNameLdapAttribute(mapperModel))) { + role.setRdnAttributeName(firstDN); + role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue()); + roles.add(role); + } + } + } + return roles; + } + + @Override + public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF); + query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF); + } + + }, + + + /** + * Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user. + * The query will be able to retrieve memberships recursively + * (Assume "role1" has member "role2" and role2 has member "johnuser". Then searching for roles of "johnuser" will return both "role1" and "role2" ) + * + * This is using AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers + */ + LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY { + + @Override + public List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { + LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider); + String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel); + membershipAttr = membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN; + String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel)); + + Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); + ldapQuery.addWhereCondition(membershipCondition); + return ldapQuery.getResultList(); + } + + @Override + public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + } + + }; + + + + public abstract List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser); + + public abstract void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query); + +} diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java index 77bc4ceb60..5631573281 100644 --- a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java +++ b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java @@ -18,5 +18,14 @@ public class LDAPDnTest { Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString()); Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn()); + + Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=keycloak, dc=org"))); + Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=org"))); + Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("DC=keycloak, DC=org"))); + Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org"))); + Assert.assertFalse(dn.isDescendantOf(dn)); + + Assert.assertEquals("uid", dn.getFirstRdnAttrName()); + Assert.assertEquals("Johny\\,Depp", dn.getFirstRdnAttrValue()); } } diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java index 79bd7b51d8..2d403f373e 100644 --- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java +++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java @@ -87,6 +87,8 @@ public class LDAPConstants { public static final String CREATE_TIMESTAMP = "createTimestamp"; public static final String MODIFY_TIMESTAMP = "modifyTimestamp"; + public static final String LDAP_MATCHING_RULE_IN_CHAIN = ":1.2.840.113556.1.4.1941:"; + public static String getUuidAttributeName(String vendor) { if (vendor != null) { switch (vendor) { From 0d52e4e6c5495d29df9c461669b51bb29ccb590f Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 16 Dec 2015 12:25:21 +0100 Subject: [PATCH 06/65] Added sync support to UserFederationMapper --- ...erationMapperSyncConfigRepresentation.java | 56 ++++++++++ ...serFederationMapperTypeRepresentation.java | 10 ++ .../mappers/AbstractLDAPFederationMapper.java | 14 +++ .../AbstractLDAPFederationMapperFactory.java | 6 + .../mappers/RoleLDAPFederationMapper.java | 105 +++++++++++++----- .../RoleLDAPFederationMapperFactory.java | 53 ++------- .../messages/admin-messages_en.properties | 6 + .../admin/resources/js/controllers/users.js | 18 ++- .../theme/base/admin/resources/js/services.js | 4 + .../partials/federated-mapper-detail.html | 2 + .../mappers/UserFederationMapper.java | 29 +++++ .../mappers/UserFederationMapperFactory.java | 13 ++- .../java/org/keycloak/models/RealmModel.java | 6 - .../models/UserFederationMapperEventImpl.java | 33 ------ .../org/keycloak/models/jpa/RealmAdapter.java | 6 - .../mongo/keycloak/adapters/RealmAdapter.java | 5 - .../admin/UserFederationProviderResource.java | 43 ++++++- .../federation/FederationTestUtils.java | 10 ++ .../federation/LDAPRoleMappingsTest.java | 3 + 19 files changed, 294 insertions(+), 128 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/idm/UserFederationMapperSyncConfigRepresentation.java delete mode 100644 model/api/src/main/java/org/keycloak/models/UserFederationMapperEventImpl.java diff --git a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperSyncConfigRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperSyncConfigRepresentation.java new file mode 100644 index 0000000000..709a616c84 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperSyncConfigRepresentation.java @@ -0,0 +1,56 @@ +package org.keycloak.representations.idm; + +/** + * @author Marek Posolda + */ +public class UserFederationMapperSyncConfigRepresentation { + + private Boolean fedToKeycloakSyncSupported; + private String fedToKeycloakSyncMessage; // applicable just if fedToKeycloakSyncSupported is true + + private Boolean keycloakToFedSyncSupported; + private String keycloakToFedSyncMessage; // applicable just if keycloakToFedSyncSupported is true + + public UserFederationMapperSyncConfigRepresentation() { + } + + public UserFederationMapperSyncConfigRepresentation(boolean fedToKeycloakSyncSupported, String fedToKeycloakSyncMessage, + boolean keycloakToFedSyncSupported, String keycloakToFedSyncMessage) { + this.fedToKeycloakSyncSupported = fedToKeycloakSyncSupported; + this.fedToKeycloakSyncMessage = fedToKeycloakSyncMessage; + this.keycloakToFedSyncSupported = keycloakToFedSyncSupported; + this.keycloakToFedSyncMessage = keycloakToFedSyncMessage; + } + + public Boolean isFedToKeycloakSyncSupported() { + return fedToKeycloakSyncSupported; + } + + public void setFedToKeycloakSyncSupported(Boolean fedToKeycloakSyncSupported) { + this.fedToKeycloakSyncSupported = fedToKeycloakSyncSupported; + } + + public String getFedToKeycloakSyncMessage() { + return fedToKeycloakSyncMessage; + } + + public void setFedToKeycloakSyncMessage(String fedToKeycloakSyncMessage) { + this.fedToKeycloakSyncMessage = fedToKeycloakSyncMessage; + } + + public Boolean isKeycloakToFedSyncSupported() { + return keycloakToFedSyncSupported; + } + + public void setKeycloakToFedSyncSupported(Boolean keycloakToFedSyncSupported) { + this.keycloakToFedSyncSupported = keycloakToFedSyncSupported; + } + + public String getKeycloakToFedSyncMessage() { + return keycloakToFedSyncMessage; + } + + public void setKeycloakToFedSyncMessage(String keycloakToFedSyncMessage) { + this.keycloakToFedSyncMessage = keycloakToFedSyncMessage; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java index f7f594af0a..48a0054f9f 100644 --- a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java @@ -12,6 +12,8 @@ public class UserFederationMapperTypeRepresentation { protected String category; protected String helpText; + protected UserFederationMapperSyncConfigRepresentation syncConfig; + protected List properties = new LinkedList<>(); public String getId() { @@ -46,6 +48,14 @@ public class UserFederationMapperTypeRepresentation { this.helpText = helpText; } + public UserFederationMapperSyncConfigRepresentation getSyncConfig() { + return syncConfig; + } + + public void setSyncConfig(UserFederationMapperSyncConfigRepresentation syncConfig) { + this.syncConfig = syncConfig; + } + public List getProperties() { return properties; } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java index 6daa011692..fd05cf87b2 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java @@ -1,12 +1,26 @@ package org.keycloak.federation.ldap.mappers; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationSyncResult; /** * @author Marek Posolda */ public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapper { + @Override + public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { + throw new IllegalStateException("Not supported"); + } + + @Override + public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { + throw new IllegalStateException("Not supported"); + } + @Override public void close() { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java index 75e77f3c3a..8dfce50c16 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java @@ -10,6 +10,7 @@ import org.keycloak.mappers.UserFederationMapperFactory; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; /** * @author Marek Posolda @@ -35,6 +36,11 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat public void postInit(KeycloakSessionFactory factory) { } + @Override + public UserFederationMapperSyncConfigRepresentation getSyncConfig() { + return new UserFederationMapperSyncConfigRepresentation(false, null, false, null); + } + @Override public void close() { } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java index 7eac273ca8..0960bb935e 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -14,12 +14,15 @@ import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.UserModelDelegate; @@ -63,15 +66,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { // Customized LDAP filter which is added to the whole LDAP query public static final String ROLES_LDAP_FILTER = "roles.ldap.filter"; - - // List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance - // TODO: Rather address this with caching at LDAPIdentityStore level? - private Set rolesSyncedModels = new TreeSet<>(); - @Override public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { - syncRolesFromLDAP(mapperModel, ldapProvider, realm); - Mode mode = getMode(mapperModel); // For now, import LDAP role mappings just during create @@ -95,34 +91,91 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { @Override public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { - syncRolesFromLDAP(mapperModel, ldapProvider, realm); } - // Sync roles from LDAP tree and create them in local Keycloak DB (if they don't exist here yet) - protected void syncRolesFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { - if (!rolesSyncedModels.contains(mapperModel.getId())) { - logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + // Sync roles from LDAP to Keycloak DB + @Override + public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { + LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; + UserFederationSyncResult syncResult = new UserFederationSyncResult() { - // Send query - List ldapRoles = ldapQuery.getResultList(); - - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); - for (LDAPObject ldapRole : ldapRoles) { - String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); - - if (roleContainer.getRole(roleName) == null) { - logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName); - roleContainer.addRole(roleName); - } + @Override + public String getStatus() { + return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated()); } - rolesSyncedModels.add(mapperModel.getId()); + }; + + logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); + + // Send LDAP query + LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + List ldapRoles = ldapQuery.getResultList(); + + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); + + if (roleContainer.getRole(roleName) == null) { + logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName); + roleContainer.addRole(roleName); + syncResult.increaseAdded(); + } else { + syncResult.increaseUpdated(); + } } + + return syncResult; } + + // Sync roles from Keycloak back to LDAP + @Override + public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { + LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; + UserFederationSyncResult syncResult = new UserFederationSyncResult() { + + @Override + public String getStatus() { + return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated()); + } + + }; + + logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); + + // Send LDAP query to see which roles exists there + LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + List ldapRoles = ldapQuery.getResultList(); + + Set ldapRoleNames = new HashSet<>(); + String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); + ldapRoleNames.add(roleName); + } + + + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + Set keycloakRoles = roleContainer.getRoles(); + + for (RoleModel keycloakRole : keycloakRoles) { + String roleName = keycloakRole.getName(); + if (ldapRoleNames.contains(roleName)) { + syncResult.increaseUpdated(); + } else { + logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName); + createLDAPRole(mapperModel, roleName, ldapProvider); + syncResult.increaseAdded(); + } + } + + return syncResult; + } + + public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { LDAPQuery ldapQuery = new LDAPQuery(ldapProvider); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java index 5cc2997a4b..455c1dbf4b 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java @@ -5,21 +5,13 @@ import java.util.LinkedList; import java.util.List; import org.jboss.logging.Logger; -import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.LDAPConstants; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; -import org.keycloak.models.UserFederationProvider; -import org.keycloak.models.UserFederationProviderFactory; -import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderEvent; -import org.keycloak.provider.ProviderEventListener; +import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; /** * @author Marek Posolda @@ -58,15 +50,15 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe membershipTypes.add(membershipType.toString()); } ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", - "DN means that LDAP role has it's members declared in form of their full DN. For example ( 'member: uid=john,ou=users,dc=example,dc=com' . " + - "UID means that LDAP role has it's members declared in form of pure user uids. For example ( 'memberUid: john' ))", + "DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " + + "UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .", ProviderConfigProperty.LIST_TYPE, membershipTypes); configProperties.add(membershipType); ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER, "LDAP Filter", - "LDAP Filter adds additional custom filter to the whole query. Make sure that it starts with '(' and ends with ')'", + "LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(ldapFilter); @@ -90,7 +82,7 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty retriever = createConfigProperty(RoleLDAPFederationMapper.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy", "Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " + "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " + - "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN extension." + "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension." , ProviderConfigProperty.LIST_TYPE, roleRetrievers); configProperties.add(retriever); @@ -131,40 +123,9 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe return PROVIDER_ID; } - // Sync roles from LDAP to Keycloak DB during creation or update of mapperModel @Override - public void postInit(KeycloakSessionFactory factory) { - factory.register(new ProviderEventListener() { - - @Override - public void onEvent(ProviderEvent event) { - if (event instanceof RealmModel.UserFederationMapperEvent) { - RealmModel.UserFederationMapperEvent mapperEvent = (RealmModel.UserFederationMapperEvent)event; - UserFederationMapperModel mapperModel = mapperEvent.getFederationMapper(); - RealmModel realm = mapperEvent.getRealm(); - KeycloakSession session = mapperEvent.getSession(); - - if (mapperModel.getFederationMapperType().equals(PROVIDER_ID)) { - try { - String federationProviderId = mapperModel.getFederationProviderId(); - UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderById(federationProviderId, realm); - if (providerModel == null) { - throw new IllegalStateException("Can't find federation provider with ID [" + federationProviderId + "] in realm " + realm.getName()); - } - - UserFederationProviderFactory ldapFactory = (UserFederationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, providerModel.getProviderName()); - LDAPFederationProvider ldapProvider = (LDAPFederationProvider) ldapFactory.getInstance(session, providerModel); - - // Sync roles - new RoleLDAPFederationMapper().syncRolesFromLDAP(mapperModel, ldapProvider, realm); - } catch (Exception e) { - logger.warn("Exception during initial sync of roles from LDAP.", e); - } - } - } - } - - }); + public UserFederationMapperSyncConfigRepresentation getSyncConfig() { + return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-roles-to-keycloak", true, "sync-keycloak-roles-to-ldap"); } @Override diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 287abe48e2..f33d6b835a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -472,6 +472,12 @@ social.default-scopes.tooltip=The scopes to be sent when asking for authorizatio key=Key stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration. +# User federation +sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak +sync-keycloak-roles-to-ldap=Sync Keycloak Roles To LDAP +sync-ldap-groups-to-keycloak=Sync LDAP Groups To Keycloak +sync-keycloak-groups-to-ldap=Sync Keycloak Groups To LDAP + realms=Realms realm=Realm diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index f42f8aa2fb..cb3aa08df1 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -986,7 +986,7 @@ module.controller('UserFederationMapperListCtrl', function($scope, $location, No }); -module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, clients, UserFederationMapper, Notifications, Dialog, $location) { +module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, clients, UserFederationMapper, UserFederationMapperSync, Notifications, Dialog, $location) { console.log('UserFederationMapperCtrl'); $scope.realm = realm; $scope.provider = provider; @@ -1035,6 +1035,22 @@ module.controller('UserFederationMapperCtrl', function($scope, realm, provider, }); }; + $scope.triggerFedToKeycloakSync = function() { + triggerMapperSync("fedToKeycloak") + } + + $scope.triggerKeycloakToFedSync = function() { + triggerMapperSync("keycloakToFed"); + } + + function triggerMapperSync(direction) { + UserFederationMapperSync.save({ direction: direction, realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) { + Notifications.success("Data synced successfully. " + syncResult.status); + }, function() { + Notifications.error("Error during sync of data"); + }); + } + }); module.controller('UserFederationMapperCreateCtrl', function($scope, realm, provider, mapperTypes, clients, UserFederationMapper, Notifications, Dialog, $location) { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 7a47a83a0c..54f1cb5bf7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -380,6 +380,10 @@ module.factory('UserFederationMapper', function($resource) { }); }); +module.factory('UserFederationMapperSync', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers/:mapperId/sync'); +}); + module.factory('UserSessionStats', function($resource) { return $resource(authUrl + '/admin/realms/:realm/users/:user/session-stats', { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html index 2c91c929f8..04d25c064c 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html @@ -53,6 +53,8 @@
+ +
diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java index 509ae64df6..2a03499262 100644 --- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java @@ -1,5 +1,10 @@ package org.keycloak.mappers; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationSyncResult; import org.keycloak.provider.Provider; /** @@ -7,4 +12,28 @@ import org.keycloak.provider.Provider; */ public interface UserFederationMapper extends Provider { + /** + * Sync data from federation storage to Keycloak. It's useful just if mapper needs some data preloaded from federation storage (For example + * load roles from federation provider and sync them to Keycloak database) + * + * Applicable just if sync is supported (see UserFederationMapperFactory.getSyncConfig() ) + * + * @see UserFederationMapperFactory#getSyncConfig() + * @param mapperModel + * @param federationProvider + * @param session + * @param realm + */ + UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm); + + /** + * Sync data from Keycloak back to federation storage + * + * @see UserFederationMapperFactory#getSyncConfig() + * @param mapperModel + * @param federationProvider + * @param session + * @param realm + */ + UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm); } diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java index a4c877683e..7690e8a106 100644 --- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java @@ -1,12 +1,9 @@ package org.keycloak.mappers; -import java.util.List; - -import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.provider.ConfiguredProvider; -import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; /** * @author Marek Posolda @@ -23,6 +20,14 @@ public interface UserFederationMapperFactory extends ProviderFactoryMarek Posolda - */ -public class UserFederationMapperEventImpl implements RealmModel.UserFederationMapperEvent { - - private final UserFederationMapperModel mapperModel; - private final RealmModel realm; - private final KeycloakSession session; - - public UserFederationMapperEventImpl(UserFederationMapperModel mapperModel, RealmModel realm, KeycloakSession session) { - this.mapperModel = mapperModel; - this.realm = realm; - this.session = session; - } - - @Override - public UserFederationMapperModel getFederationMapper() { - return mapperModel; - } - - @Override - public RealmModel getRealm() { - return realm; - } - - public KeycloakSession getSession() { - return session; - } -} 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 4f96829e61..bebe07990f 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 @@ -1,6 +1,5 @@ package org.keycloak.models.jpa; -import org.keycloak.Config; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.common.enums.SslRequired; import org.keycloak.models.AuthenticationExecutionModel; @@ -19,7 +18,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.UserFederationMapperEventImpl; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderCreationEventImpl; import org.keycloak.models.UserFederationProviderModel; @@ -1541,8 +1539,6 @@ public class RealmAdapter implements RealmModel { this.realm.getUserFederationMappers().add(entity); UserFederationMapperModel mapperModel = entityToModel(entity); - session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapperModel, this, session)); - return mapperModel; } @@ -1597,8 +1593,6 @@ public class RealmAdapter implements RealmModel { entity.getConfig().putAll(mapper.getConfig()); } em.flush(); - - session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapper, this, session)); } @Override diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index c3b159a327..6a1a84d0bf 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -22,7 +22,6 @@ import org.keycloak.models.RealmProvider; import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.UserFederationMapperEventImpl; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderCreationEventImpl; import org.keycloak.models.UserFederationProviderModel; @@ -1930,8 +1929,6 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateMongoEntity(); UserFederationMapperModel mapperModel = entityToModel(entity); - session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapperModel, this, session)); - return mapperModel; } @@ -1986,8 +1983,6 @@ public class RealmAdapter extends AbstractMongoAdapter impleme entity.getConfig().putAll(mapper.getConfig()); } updateMongoEntity(); - - session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapper, this, session)); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java index 6f7bedfd82..3c4226a7aa 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java @@ -32,8 +32,11 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.provider.ProviderConfigProperty; @@ -138,11 +141,13 @@ public class UserFederationProviderResource { auth.requireManage(); UsersSyncManager syncManager = new UsersSyncManager(); - UserFederationSyncResult syncResult = null; + UserFederationSyncResult syncResult; if ("triggerFullSync".equals(action)) { syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel); } else if ("triggerChangedUsersSync".equals(action)) { syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel); + } else { + throw new NotFoundException("Unknown action: " + action); } adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); @@ -172,6 +177,7 @@ public class UserFederationProviderResource { rep.setCategory(mapperFactory.getDisplayCategory()); rep.setName(mapperFactory.getDisplayType()); rep.setHelpText(mapperFactory.getHelpText()); + rep.setSyncConfig(mapperFactory.getSyncConfig()); List configProperties = mapperFactory.getConfigProperties(); for (ProviderConfigProperty prop : configProperties) { ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation(); @@ -307,6 +313,41 @@ public class UserFederationProviderResource { } + /** + * Trigger sync of mapper data related to federationMapper (roles, groups, ...) + * + * @return + */ + @POST + @Path("mappers/{id}/sync") + @NoCache + public UserFederationSyncResult syncMapperData(@PathParam("id") String mapperId, @QueryParam("direction") String direction) { + auth.requireManage(); + + UserFederationMapperModel mapperModel = realm.getUserFederationMapperById(mapperId); + if (mapperModel == null) throw new NotFoundException("Mapper model not found"); + UserFederationMapper mapper = session.getProvider(UserFederationMapper.class, mapperModel.getFederationMapperType()); + + UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderById(mapperModel.getFederationProviderId(), realm); + if (providerModel == null) throw new NotFoundException("Provider model not found"); + UserFederationProviderFactory providerFactory = (UserFederationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, providerModel.getProviderName()); + UserFederationProvider federationProvider = providerFactory.getInstance(session, providerModel); + + logger.infof("Syncing data for mapper '%s' of type '%s'. Direction: %s", mapperModel.getName(), mapperModel.getFederationMapperType(), direction); + + UserFederationSyncResult syncResult; + if ("fedToKeycloak".equals(direction)) { + syncResult = mapper.syncDataFromFederationProviderToKeycloak(mapperModel, federationProvider, session, realm); + } else if ("keycloakToFed".equals(direction)) { + syncResult = mapper.syncDataFromKeycloakToFederationProvider(mapperModel, federationProvider, session, realm); + } else { + throw new NotFoundException("Unknown direction: " + direction); + } + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); + return syncResult; + } + private void validateModel(UserFederationMapperModel model) { try { UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java index 63a27e6ce7..580b3456c9 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java @@ -141,6 +141,16 @@ class FederationTestUtils { } } + public static void syncRolesFromLDAP(RealmModel realm, LDAPFederationProvider ldapProvider, UserFederationProviderModel providerModel) { + RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); + + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper"); + roleMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, ldapProvider.getSession(), realm); + + mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper"); + roleMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, ldapProvider.getSession(), realm); + } + public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) { LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore(); LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java index 5bb2291547..2d6eac90dc 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java @@ -88,6 +88,9 @@ public class LDAPRoleMappingsTest { FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1"); FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole2"); FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "financeRolesMapper", "financeRole1"); + + // Sync LDAP roles to Keycloak DB + FederationTestUtils.syncRolesFromLDAP(appRealm, ldapFedProvider, ldapModel); } }); From d0ec1c3b023393ff84a067e25868ecf2ba1558e6 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 16 Dec 2015 14:35:50 +0100 Subject: [PATCH 07/65] Mongo fix --- .../connections/mongo/DefaultMongoConnectionFactoryProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 67cb127a90..e76e93f446 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -37,6 +37,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", "org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.mongo.keycloak.entities.MongoOnlineUserSessionEntity", From 0527d441e3aa7f0986a403da6d5258fc37939a13 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 16 Dec 2015 12:23:41 -0500 Subject: [PATCH 08/65] better logging --- .../AuthenticationProcessor.java | 6 +++- .../DefaultAuthenticationFlow.java | 35 ++++++++++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 710c77d0e3..75803f1a6d 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -658,6 +658,7 @@ public class AuthenticationProcessor { } public Response authenticateClient() throws AuthenticationFlowException { + logger.debug("AUTHENTICATE CLIENT"); AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null); try { Response challenge = authenticationFlow.processFlow(); @@ -693,6 +694,7 @@ public class AuthenticationProcessor { } public static void resetFlow(ClientSessionModel clientSession) { + logger.debug("RESET FLOW"); clientSession.setTimestamp(Time.currentTime()); clientSession.setAuthenticatedUser(null); clientSession.clearExecutionStatus(); @@ -715,6 +717,7 @@ public class AuthenticationProcessor { public Response authenticationAction(String execution) { + logger.debug("authenticationAction"); checkClientSession(); String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION); if (!execution.equals(current)) { @@ -762,7 +765,8 @@ public class AuthenticationProcessor { } public Response authenticateOnly() throws AuthenticationFlowException { - checkClientSession(); + logger.debug("AUTHENTICATE ONLY"); + checkClientSession(); event.client(clientSession.getClient().getClientId()) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) .detail(Details.AUTH_METHOD, clientSession.getAuthMethod()); diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index ab27bbb9f5..9a3a8c221a 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -1,5 +1,6 @@ package org.keycloak.authentication; +import org.jboss.logging.Logger; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientSessionModel; @@ -17,6 +18,7 @@ import java.util.List; * @version $Revision: 1 $ */ public class DefaultAuthenticationFlow implements AuthenticationFlow { + protected static Logger logger = Logger.getLogger(DefaultAuthenticationFlow.class); Response alternativeChallenge = null; AuthenticationExecutionModel challengedAlternativeExecution = null; boolean alternativeSuccessful = false; @@ -44,10 +46,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { @Override public Response processAction(String actionExecution) { + logger.debugv("processAction: {0}", actionExecution); while (executionIterator.hasNext()) { AuthenticationExecutionModel model = executionIterator.next(); + logger.debugv("check: {0} requirement: {1}", model.getAuthenticator(), model.getRequirement().toString()); if (isProcessed(model)) { - AuthenticationProcessor.logger.debug("execution is processed"); + logger.debug("execution is processed"); if (!alternativeSuccessful && model.isAlternative() && processor.isSuccessful(model)) alternativeSuccessful = true; continue; @@ -62,6 +66,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } Authenticator authenticator = factory.create(processor.getSession()); AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions); + logger.debugv("action: {0}", model.getAuthenticator()); authenticator.action(result); Response response = processResult(result); if (response == null) { @@ -80,19 +85,24 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { @Override public Response processFlow() { + logger.debug("processFlow"); while (executionIterator.hasNext()) { AuthenticationExecutionModel model = executionIterator.next(); + logger.debugv("check execution: {0} requirement: {1}", model.getAuthenticator(), model.getRequirement().toString()); + if (isProcessed(model)) { - AuthenticationProcessor.logger.debug("execution is processed"); + logger.debug("execution is processed"); if (!alternativeSuccessful && model.isAlternative() && processor.isSuccessful(model)) alternativeSuccessful = true; continue; } if (model.isAlternative() && alternativeSuccessful) { + logger.debug("Skip alternative execution"); processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); continue; } if (model.isAuthenticatorFlow()) { + logger.debug("execution is flow"); AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); Response flowChallenge = authenticationFlow.processFlow(); if (flowChallenge == null) { @@ -122,7 +132,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?"); } Authenticator authenticator = factory.create(processor.getSession()); - AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId()); + logger.debugv("authenticator: {0}", factory.getId()); UserModel authUser = processor.getClientSession().getAuthenticatedUser(); if (authenticator.requiresUser() && authUser == null) { @@ -138,7 +148,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { if (!configuredFor) { if (model.isRequired()) { if (factory.isUserSetupAllowed()) { - AuthenticationProcessor.logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId()); + logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId()); processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SETUP_REQUIRED); authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getClientSession().getAuthenticatedUser()); continue; @@ -152,6 +162,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } } AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions); + logger.debug("invoke authenticator.authenticate"); authenticator.authenticate(context); Response response = processResult(context); if (response != null) return response; @@ -165,12 +176,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { FlowStatus status = result.getStatus(); switch (status) { case SUCCESS: - AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); + logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); if (execution.isAlternative()) alternativeSuccessful = true; return null; case FAILED: - AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); + logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); processor.logFailure(); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); if (result.getChallenge() != null) { @@ -178,14 +189,14 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } throw new AuthenticationFlowException(result.getError()); case FORK: - AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); + logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); case FORCE_CHALLENGE: processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); case CHALLENGE: - AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); + logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); if (execution.isRequired()) { processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); @@ -203,12 +214,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } return null; case FAILURE_CHALLENGE: - AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); + logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); processor.logFailure(); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); case ATTEMPTED: - AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); + logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); } @@ -218,8 +229,8 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { AuthenticationProcessor.resetFlow(processor.getClientSession()); return processor.authenticate(); default: - AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); - AuthenticationProcessor.logger.error("Unknown result status"); + logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); + logger.error("Unknown result status"); throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR); } } From 1b614a379bf558f5e4231427817b49ff0de594f7 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 16 Dec 2015 18:46:52 -0200 Subject: [PATCH 09/65] [KEYCLOAK-2202] - Initial support for SAML ECP Profile. --- .../keycloak-saml-protocol/main/module.xml | 1 + .../utils/DefaultAuthenticationFlows.java | 24 + ...eLogin.java => AbstractInitiateLogin.java} | 21 +- .../adapters/saml/OnSessionCreated.java | 9 + .../adapters/saml/SamlAuthenticator.java | 525 +----------------- .../AbstractSamlAuthenticationHandler.java | 484 ++++++++++++++++ .../profile/SamlAuthenticationHandler.java | 13 + .../saml/profile/SamlInvocationContext.java | 37 ++ .../profile/ecp/EcpAuthenticationHandler.java | 146 +++++ .../WebBrowserSsoAuthenticationHandler.java | 111 ++++ .../common/constants/JBossSAMLConstants.java | 3 +- .../constants/JBossSAMLURIConstants.java | 7 +- .../keycloak/protocol/saml/SamlProtocol.java | 43 +- .../protocol/saml/SamlProtocolFactory.java | 9 +- .../keycloak/protocol/saml/SamlService.java | 32 +- .../ecp/SamlEcpProfileProtocolFactory.java | 109 ++++ .../profile/ecp/SamlEcpProfileService.java | 70 +++ .../authenticator/HttpBasicAuthenticator.java | 174 ++++++ .../protocol/saml/profile/ecp/util/Soap.java | 177 ++++++ ...ycloak.authentication.AuthenticatorFactory | 1 + ...org.keycloak.protocol.LoginProtocolFactory | 3 +- .../protocol/AuthorizationEndpointBase.java | 6 +- .../testsuite/saml/SamlEcpProfileTest.java | 230 ++++++++ .../ecp/ecp-sp/WEB-INF/keycloak-saml.xml | 40 ++ .../ecp/ecp-sp/WEB-INF/keystore.jks | Bin 0 -> 1705 bytes .../keycloak-saml/ecp/testsamlecp.json | 67 +++ 26 files changed, 1799 insertions(+), 543 deletions(-) rename saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/{InitiateLogin.java => AbstractInitiateLogin.java} (79%) create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java create mode 100755 saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml index fbd65fd5cb..81cd365853 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml @@ -26,6 +26,7 @@ + diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index 3a105c46e2..c49f5b6b9b 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -24,6 +24,7 @@ public class DefaultAuthenticationFlows { public static final String DIRECT_GRANT_FLOW = "direct grant"; 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 CLIENT_AUTHENTICATION_FLOW = "clients"; public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login"; @@ -39,6 +40,7 @@ public class DefaultAuthenticationFlows { if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm); 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); } public static void migrateFlows(RealmModel realm) { if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true); @@ -47,6 +49,7 @@ public class DefaultAuthenticationFlows { if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm); 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); } public static void registrationFlow(RealmModel realm) { @@ -447,4 +450,25 @@ public class DefaultAuthenticationFlows { execution.setAuthenticatorFlow(false); realm.addAuthenticatorExecution(execution); } + + public static void samlEcpProfile(RealmModel realm) { + AuthenticationFlowModel ecpFlow = new AuthenticationFlowModel(); + + ecpFlow.setAlias(SAML_ECP_FLOW); + ecpFlow.setDescription("SAML ECP Profile Authentication Flow"); + ecpFlow.setProviderId("basic-flow"); + ecpFlow.setTopLevel(true); + ecpFlow.setBuiltIn(true); + ecpFlow = realm.addAuthenticationFlow(ecpFlow); + + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + + execution.setParentFlow(ecpFlow.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator("http-basic-authenticator"); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + } } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java similarity index 79% rename from saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java rename to saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java index 4c7cbab5e0..2496b7c49f 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java @@ -7,21 +7,24 @@ import org.keycloak.saml.BaseSAML2BindingBuilder; import org.keycloak.saml.SAML2AuthnRequestBuilder; import org.keycloak.saml.SAML2NameIDPolicyBuilder; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ProcessingException; import org.w3c.dom.Document; +import java.io.IOException; import java.security.KeyPair; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class InitiateLogin implements AuthChallenge { - protected static Logger log = Logger.getLogger(InitiateLogin.class); +public abstract class AbstractInitiateLogin implements AuthChallenge { + protected static Logger log = Logger.getLogger(AbstractInitiateLogin.class); protected SamlDeployment deployment; protected SamlSessionStore sessionStore; - public InitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) { + public AbstractInitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) { this.deployment = deployment; this.sessionStore = sessionStore; } @@ -35,18 +38,14 @@ public class InitiateLogin implements AuthChallenge { public boolean challenge(HttpFacade httpFacade) { try { String issuerURL = deployment.getEntityID(); - String actionUrl = deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(); - String destinationUrl = actionUrl; String nameIDPolicyFormat = deployment.getNameIDPolicyFormat(); if (nameIDPolicyFormat == null) { nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); } - - SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder() - .destination(destinationUrl) + .destination(deployment.getIDP().getSingleSignOnService().getRequestBindingUrl()) .issuer(issuerURL) .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive()) .nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat)); @@ -79,9 +78,7 @@ public class InitiateLogin implements AuthChallenge { } sessionStore.saveRequest(); - Document document = authnRequestBuilder.toDocument(); - SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding(); - SamlUtil.sendSaml(true, httpFacade, actionUrl, binding, document, samlBinding); + sendAuthnRequest(httpFacade, authnRequestBuilder, binding); sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN); } catch (Exception e) { throw new RuntimeException("Could not create authentication request.", e); @@ -89,4 +86,6 @@ public class InitiateLogin implements AuthChallenge { return true; } + protected abstract void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException; + } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java new file mode 100644 index 0000000000..92f3b4db3e --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java @@ -0,0 +1,9 @@ +package org.keycloak.adapters.saml; + +/** + * @author Pedro Igor + */ +public interface OnSessionCreated { + + void onSessionCreated(SamlSession samlSession); +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java index 02097db12c..cd9affd674 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java @@ -1,530 +1,49 @@ package org.keycloak.adapters.saml; import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; +import org.keycloak.adapters.saml.profile.ecp.EcpAuthenticationHandler; +import org.keycloak.adapters.saml.profile.webbrowsersso.WebBrowserSsoAuthenticationHandler; 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.KeycloakUriBuilder; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.dom.saml.v2.assertion.AssertionType; -import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.dom.saml.v2.assertion.AttributeType; -import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; -import org.keycloak.dom.saml.v2.assertion.NameIDType; -import org.keycloak.dom.saml.v2.assertion.StatementAbstractType; -import org.keycloak.dom.saml.v2.assertion.SubjectType; -import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; -import org.keycloak.dom.saml.v2.protocol.RequestAbstractType; -import org.keycloak.dom.saml.v2.protocol.ResponseType; -import org.keycloak.dom.saml.v2.protocol.StatusCodeType; -import org.keycloak.dom.saml.v2.protocol.StatusResponseType; -import org.keycloak.dom.saml.v2.protocol.StatusType; -import org.keycloak.saml.BaseSAML2BindingBuilder; -import org.keycloak.saml.SAML2LogoutRequestBuilder; -import org.keycloak.saml.SAML2LogoutResponseBuilder; -import org.keycloak.saml.SAMLRequestParser; -import org.keycloak.saml.SignatureAlgorithm; -import org.keycloak.saml.common.constants.GeneralConstants; -import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.common.util.Base64; -import org.keycloak.saml.common.util.StringUtil; -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.util.AssertionUtil; -import org.keycloak.saml.processing.web.util.PostBindingUtil; -import org.w3c.dom.Document; -import org.w3c.dom.Node; - -import java.net.URI; -import java.security.PublicKey; -import java.security.Signature; -import java.util.HashSet; -import java.util.List; -import java.util.Set; /** * @author Bill Burke * @version $Revision: 1 $ */ public abstract class SamlAuthenticator { + protected static Logger log = Logger.getLogger(SamlAuthenticator.class); - protected HttpFacade facade; - protected AuthChallenge challenge; - protected SamlDeployment deployment; - protected SamlSessionStore sessionStore; + private final SamlAuthenticationHandler handler; - public SamlAuthenticator(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - this.facade = facade; - this.deployment = deployment; - this.sessionStore = sessionStore; + public SamlAuthenticator(final HttpFacade facade, final SamlDeployment deployment, final SamlSessionStore sessionStore) { + this.handler = createAuthenticationHandler(facade, deployment, sessionStore); } public AuthChallenge getChallenge() { - return challenge; + return this.handler.getChallenge(); } public AuthOutcome authenticate() { - - - String samlRequest = facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY); - String samlResponse = facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY); - String relayState = facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE); - boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO")); - if (samlRequest != null) { - return handleSamlRequest(samlRequest, relayState); - } else if (samlResponse != null) { - return handleSamlResponse(samlResponse, relayState); - } else if (sessionStore.isLoggedIn()) { - if (globalLogout) { - return globalLogout(); + log.debugf("SamlAuthenticator is using handler [%s]", this.handler); + return this.handler.handle(new OnSessionCreated() { + @Override + public void onSessionCreated(SamlSession samlSession) { + completeAuthentication(samlSession); } - if (verifySSL()) return AuthOutcome.FAILED; - log.debug("AUTHENTICATED: was cached"); - return AuthOutcome.AUTHENTICATED; - } - return initiateLogin(); + }); } - protected AuthOutcome globalLogout() { - SamlSession account = sessionStore.getAccount(); - if (account == null) { - return AuthOutcome.NOT_ATTEMPTED; - } - SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() - .assertionExpiration(30) - .issuer(deployment.getEntityID()) - .sessionIndex(account.getSessionIndex()) - .userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat()) - .destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl()); - BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder(); - if (deployment.getIDP().getSingleLogoutService().signRequest()) { - binding.signWith(deployment.getSigningKeyPair()) - .signDocument(); + protected abstract void completeAuthentication(SamlSession samlSession); + + private SamlAuthenticationHandler createAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + if (EcpAuthenticationHandler.canHandle(facade)) { + return EcpAuthenticationHandler.create(facade, deployment, sessionStore); } - binding.relayState("logout"); - - try { - SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding()); - sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT); - } catch (Exception e) { - log.error("Could not send global logout SAML request", e); - return AuthOutcome.FAILED; - } - return AuthOutcome.NOT_ATTEMPTED; + // defaults to the web browser sso profile + return WebBrowserSsoAuthenticationHandler.create(facade, deployment, sessionStore); } - - protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) { - SAMLDocumentHolder holder = null; - boolean postBinding = false; - String requestUri = facade.getRequest().getURI(); - if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) { - // strip out query params - int index = requestUri.indexOf('?'); - if (index > -1) { - requestUri = requestUri.substring(0, index); - } - holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest); - } else { - postBinding = true; - holder = SAMLRequestParser.parseRequestPostBinding(samlRequest); - } - RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject(); - if (!requestUri.equals(requestAbstractType.getDestination().toString())) { - log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'"); - return AuthOutcome.FAILED; - } - - if (requestAbstractType instanceof LogoutRequestType) { - if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) { - try { - validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY); - } catch (VerificationException e) { - log.error("Failed to verify saml request signature", e); - return AuthOutcome.FAILED; - } - } - LogoutRequestType logout = (LogoutRequestType) requestAbstractType; - return logoutRequest(logout, relayState); - - } else { - log.error("unknown SAML request type"); - return AuthOutcome.FAILED; - } - } - - protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) { - if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) { - sessionStore.logoutByPrincipal(request.getNameID().getValue()); - } else { - sessionStore.logoutBySsoId(request.getSessionIndex()); - } - - String issuerURL = deployment.getEntityID(); - SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder(); - builder.logoutRequestID(request.getID()); - builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl()); - builder.issuer(issuerURL); - BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState); - if (deployment.getIDP().getSingleLogoutService().signResponse()) { - binding.signatureAlgorithm(deployment.getSignatureAlgorithm()) - .signWith(deployment.getSigningKeyPair()) - .signDocument(); - if (deployment.getSignatureCanonicalizationMethod() != null) - binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod()); - } - - - try { - SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(), - deployment.getIDP().getSingleLogoutService().getResponseBinding()); - } catch (Exception e) { - log.error("Could not send logout response SAML request", e); - return AuthOutcome.FAILED; - } - return AuthOutcome.NOT_ATTEMPTED; - - } - - - protected AuthOutcome handleSamlResponse(String samlResponse, String relayState) { - SAMLDocumentHolder holder = null; - boolean postBinding = false; - String requestUri = facade.getRequest().getURI(); - if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) { - int index = requestUri.indexOf('?'); - if (index > -1) { - requestUri = requestUri.substring(0, index); - } - holder = extractRedirectBindingResponse(samlResponse); - } else { - postBinding = true; - holder = extractPostBindingResponse(samlResponse); - } - final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject(); - // validate destination - if (!requestUri.equals(statusResponse.getDestination())) { - log.error("Request URI does not match SAML request destination"); - return AuthOutcome.FAILED; - } - - if (statusResponse instanceof ResponseType) { - try { - if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) { - try { - validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY); - } catch (VerificationException e) { - log.error("Failed to verify saml response signature", e); - - challenge = new AuthChallenge() { - @Override - public boolean challenge(HttpFacade exchange) { - SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE); - exchange.getRequest().setError(error); - exchange.getResponse().sendError(403); - return true; - } - - @Override - public int getResponseCode() { - return 403; - } - }; - return AuthOutcome.FAILED; - } - } - return handleLoginResponse((ResponseType) statusResponse); - } finally { - sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE); - } - - } else { - if (sessionStore.isLoggingOut()) { - try { - if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) { - try { - validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY); - } catch (VerificationException e) { - log.error("Failed to verify saml response signature", e); - return AuthOutcome.FAILED; - } - } - return handleLogoutResponse(holder, statusResponse, relayState); - } finally { - sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE); - } - - } else if (sessionStore.isLoggingIn()) { - - try { - // KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly. - StatusType status = statusResponse.getStatus(); - if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){ - log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString()); - return AuthOutcome.NOT_AUTHENTICATED; - } - - challenge = new AuthChallenge() { - @Override - public boolean challenge(HttpFacade exchange) { - SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse); - exchange.getRequest().setError(error); - exchange.getResponse().sendError(403); - return true; - } - - @Override - public int getResponseCode() { - return 403; - } - }; - return AuthOutcome.FAILED; - } finally { - sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE); - } - } - return AuthOutcome.NOT_ATTEMPTED; - } - - } - - private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException { - if (postBinding) { - verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey()); - } else { - verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey); - } - } - - private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){ - if(statusCode != null && statusCode.getValue()!=null){ - String v = statusCode.getValue().toString(); - return expectedValue.equals(v); - } - return false; - } - - protected AuthOutcome handleLoginResponse(ResponseType responseType) { - - AssertionType assertion = null; - try { - assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey()); - if (AssertionUtil.hasExpired(assertion)) { - return initiateLogin(); - } - } catch (Exception e) { - log.error("Error extracting SAML assertion: " + e.getMessage()); - challenge = new AuthChallenge() { - @Override - public boolean challenge(HttpFacade exchange) { - SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE); - exchange.getRequest().setError(error); - exchange.getResponse().sendError(403); - return true; - } - - @Override - public int getResponseCode() { - return 403; - } - }; - } - - SubjectType subject = assertion.getSubject(); - SubjectType.STSubType subType = subject.getSubType(); - NameIDType subjectNameID = (NameIDType) subType.getBaseID(); - String principalName = subjectNameID.getValue(); - - final Set roles = new HashSet<>(); - MultivaluedHashMap attributes = new MultivaluedHashMap<>(); - MultivaluedHashMap friendlyAttributes = new MultivaluedHashMap<>(); - - Set statements = assertion.getStatements(); - for (StatementAbstractType statement : statements) { - if (statement instanceof AttributeStatementType) { - AttributeStatementType attributeStatement = (AttributeStatementType) statement; - List attList = attributeStatement.getAttributes(); - for (AttributeStatementType.ASTChoiceType obj : attList) { - AttributeType attr = obj.getAttribute(); - if (isRole(attr)) { - List attributeValues = attr.getAttributeValue(); - if (attributeValues != null) { - for (Object attrValue : attributeValues) { - String role = getAttributeValue(attrValue); - log.debugv("Add role: {0}", role); - roles.add(role); - } - } - } else { - List attributeValues = attr.getAttributeValue(); - if (attributeValues != null) { - for (Object attrValue : attributeValues) { - String value = getAttributeValue(attrValue); - if (attr.getName() != null) { - attributes.add(attr.getName(), value); - } - if (attr.getFriendlyName() != null) { - friendlyAttributes.add(attr.getFriendlyName(), value); - } - } - } - } - - } - } - } - if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) { - if (deployment.getPrincipalAttributeName() != null) { - String attribute = attributes.getFirst(deployment.getPrincipalAttributeName()); - if (attribute != null) principalName = attribute; - else { - attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName()); - if (attribute != null) principalName = attribute; - } - } - } - - AuthnStatementType authn = null; - for (Object statement : assertion.getStatements()) { - if (statement instanceof AuthnStatementType) { - authn = (AuthnStatementType) statement; - break; - } - } - - - URI nameFormat = 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(); - final String sessionIndex = index; - SamlSession account = new SamlSession(principal, roles, sessionIndex); - sessionStore.saveAccount(account); - completeAuthentication(account); - - - // redirect to original request, it will be restored - String redirectUri = sessionStore.getRedirectUri(); - if (redirectUri != null) { - facade.getResponse().setHeader("Location", redirectUri); - facade.getResponse().setStatus(302); - facade.getResponse().end(); - } else { - log.debug("IDP initiated invocation"); - } - log.debug("AUTHENTICATED authn"); - - return AuthOutcome.AUTHENTICATED; - } - - protected abstract void completeAuthentication(SamlSession account); - - private String getAttributeValue(Object attrValue) { - String value = null; - if (attrValue instanceof String) { - value = (String) attrValue; - } else if (attrValue instanceof Node) { - Node roleNode = (Node) attrValue; - value = roleNode.getFirstChild().getNodeValue(); - } else if (attrValue instanceof NameIDType) { - NameIDType nameIdType = (NameIDType) attrValue; - value = nameIdType.getValue(); - } else { - log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName()); - } - return value; - } - - protected boolean isRole(AttributeType attribute) { - return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName())); - } - - protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) { - boolean loggedIn = sessionStore.isLoggedIn(); - if (!loggedIn || !"logout".equals(relayState)) { - return AuthOutcome.NOT_ATTEMPTED; - } - sessionStore.logoutAccount(); - return AuthOutcome.LOGGED_OUT; - } - - protected SAMLDocumentHolder extractRedirectBindingResponse(String response) { - return SAMLRequestParser.parseRequestRedirectBinding(response); - } - - - protected SAMLDocumentHolder extractPostBindingResponse(String response) { - byte[] samlBytes = PostBindingUtil.base64Decode(response); - return SAMLRequestParser.parseResponseDocument(samlBytes); - } - - - protected AuthOutcome initiateLogin() { - challenge = new InitiateLogin(deployment, sessionStore); - return AuthOutcome.NOT_ATTEMPTED; - } - - protected boolean verifySSL() { - if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { - log.warn("SSL is required to authenticate"); - return true; - } - return false; - } - - public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException { - SAML2Signature saml2Signature = new SAML2Signature(); - try { - if (!saml2Signature.validate(document, publicKey)) { - throw new VerificationException("Invalid signature on document"); - } - } catch (ProcessingException e) { - throw new VerificationException("Error validating signature", e); - } - } - - public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException { - String request = facade.getRequest().getQueryParamValue(paramKey); - String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); - String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY); - String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); - - if (request == null) { - throw new VerificationException("SAML Request was null"); - } - if (algorithm == null) throw new VerificationException("SigAlg was null"); - if (signature == null) throw new VerificationException("Signature was null"); - - // Shibboleth doesn't sign the document for redirect binding. - // todo maybe a flag? - - String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE); - KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/") - .queryParam(paramKey, request); - if (relayState != null) { - builder.queryParam(GeneralConstants.RELAY_STATE, relayState); - } - builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm); - String rawQuery = builder.build().getRawQuery(); - - try { - //byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature); - byte[] decodedSignature = Base64.decode(signature); - - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm); - Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg - validator.initVerify(publicKey); - validator.update(rawQuery.getBytes("UTF-8")); - if (!validator.verify(decodedSignature)) { - throw new VerificationException("Invalid query param signature"); - } - } catch (Exception e) { - throw new VerificationException(e); - } - } - - -} +} \ No newline at end of file diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java new file mode 100644 index 0000000000..290b2d7eb4 --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java @@ -0,0 +1,484 @@ +package org.keycloak.adapters.saml.profile; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.AbstractInitiateLogin; +import org.keycloak.adapters.saml.OnSessionCreated; +import org.keycloak.adapters.saml.SamlAuthenticationError; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlPrincipal; +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.saml.SamlUtil; +import org.keycloak.adapters.saml.profile.webbrowsersso.WebBrowserSsoAuthenticationHandler; +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.KeycloakUriBuilder; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.dom.saml.v2.assertion.AssertionType; +import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; +import org.keycloak.dom.saml.v2.assertion.AttributeType; +import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; +import org.keycloak.dom.saml.v2.assertion.NameIDType; +import org.keycloak.dom.saml.v2.assertion.StatementAbstractType; +import org.keycloak.dom.saml.v2.assertion.SubjectType; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.dom.saml.v2.protocol.RequestAbstractType; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusCodeType; +import org.keycloak.dom.saml.v2.protocol.StatusResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusType; +import org.keycloak.saml.BaseSAML2BindingBuilder; +import org.keycloak.saml.SAML2AuthnRequestBuilder; +import org.keycloak.saml.SAMLRequestParser; +import org.keycloak.saml.SignatureAlgorithm; +import org.keycloak.saml.common.constants.GeneralConstants; +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.Base64; +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.util.AssertionUtil; +import org.keycloak.saml.processing.web.util.PostBindingUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import java.io.IOException; +import java.net.URI; +import java.security.PublicKey; +import java.security.Signature; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * + * @author Bill Burke + */ +public abstract class AbstractSamlAuthenticationHandler implements SamlAuthenticationHandler { + + protected static Logger log = Logger.getLogger(WebBrowserSsoAuthenticationHandler.class); + + protected final HttpFacade facade; + protected final SamlSessionStore sessionStore; + protected final SamlDeployment deployment; + protected AuthChallenge challenge; + + public AbstractSamlAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + this.facade = facade; + this.deployment = deployment; + this.sessionStore = sessionStore; + } + + public AuthOutcome doHandle(SamlInvocationContext context, OnSessionCreated onCreateSession) { + String samlRequest = context.getSamlRequest(); + String samlResponse = context.getSamlResponse(); + String relayState = context.getRelayState(); + if (samlRequest != null) { + return handleSamlRequest(samlRequest, relayState); + } else if (samlResponse != null) { + return handleSamlResponse(samlResponse, relayState, onCreateSession); + } else if (sessionStore.isLoggedIn()) { + if (verifySSL()) return AuthOutcome.FAILED; + log.debug("AUTHENTICATED: was cached"); + return handleRequest(); + } + return initiateLogin(); + } + + protected AuthOutcome handleRequest() { + return AuthOutcome.AUTHENTICATED; + } + + @Override + public AuthChallenge getChallenge() { + return this.challenge; + } + + protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) { + SAMLDocumentHolder holder = null; + boolean postBinding = false; + String requestUri = facade.getRequest().getURI(); + if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) { + // strip out query params + int index = requestUri.indexOf('?'); + if (index > -1) { + requestUri = requestUri.substring(0, index); + } + holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest); + } else { + postBinding = true; + holder = SAMLRequestParser.parseRequestPostBinding(samlRequest); + } + RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject(); + if (!requestUri.equals(requestAbstractType.getDestination().toString())) { + log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'"); + return AuthOutcome.FAILED; + } + + if (requestAbstractType instanceof LogoutRequestType) { + if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) { + try { + validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY); + } catch (VerificationException e) { + log.error("Failed to verify saml request signature", e); + return AuthOutcome.FAILED; + } + } + LogoutRequestType logout = (LogoutRequestType) requestAbstractType; + return logoutRequest(logout, relayState); + + } else { + log.error("unknown SAML request type"); + return AuthOutcome.FAILED; + } + } + + protected abstract AuthOutcome logoutRequest(LogoutRequestType request, String relayState); + + protected AuthOutcome handleSamlResponse(String samlResponse, String relayState, OnSessionCreated onCreateSession) { + SAMLDocumentHolder holder = null; + boolean postBinding = false; + String requestUri = facade.getRequest().getURI(); + if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) { + int index = requestUri.indexOf('?'); + if (index > -1) { + requestUri = requestUri.substring(0, index); + } + holder = extractRedirectBindingResponse(samlResponse); + } else { + postBinding = true; + holder = extractPostBindingResponse(samlResponse); + } + final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject(); + // validate destination + if (!requestUri.equals(statusResponse.getDestination())) { + log.error("Request URI does not match SAML request destination"); + return AuthOutcome.FAILED; + } + + if (statusResponse instanceof ResponseType) { + try { + if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) { + try { + validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY); + } catch (VerificationException e) { + log.error("Failed to verify saml response signature", e); + + challenge = new AuthChallenge() { + @Override + public boolean challenge(HttpFacade exchange) { + SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE); + exchange.getRequest().setError(error); + exchange.getResponse().sendError(403); + return true; + } + + @Override + public int getResponseCode() { + return 403; + } + }; + return AuthOutcome.FAILED; + } + } + return handleLoginResponse((ResponseType) statusResponse, onCreateSession); + } finally { + sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE); + } + + } else { + if (sessionStore.isLoggingOut()) { + try { + if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) { + try { + validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY); + } catch (VerificationException e) { + log.error("Failed to verify saml response signature", e); + return AuthOutcome.FAILED; + } + } + return handleLogoutResponse(holder, statusResponse, relayState); + } finally { + sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE); + } + + } else if (sessionStore.isLoggingIn()) { + + try { + // KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly. + StatusType status = statusResponse.getStatus(); + if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){ + log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString()); + return AuthOutcome.NOT_AUTHENTICATED; + } + + challenge = new AuthChallenge() { + @Override + public boolean challenge(HttpFacade exchange) { + SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse); + exchange.getRequest().setError(error); + exchange.getResponse().sendError(403); + return true; + } + + @Override + public int getResponseCode() { + return 403; + } + }; + return AuthOutcome.FAILED; + } finally { + sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE); + } + } + return AuthOutcome.NOT_ATTEMPTED; + } + + } + + private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException { + if (postBinding) { + verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey()); + } else { + verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey); + } + } + + private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){ + if(statusCode != null && statusCode.getValue()!=null){ + String v = statusCode.getValue().toString(); + return expectedValue.equals(v); + } + return false; + } + + protected AuthOutcome handleLoginResponse(ResponseType responseType, OnSessionCreated onCreateSession) { + + AssertionType assertion = null; + try { + assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey()); + if (AssertionUtil.hasExpired(assertion)) { + return initiateLogin(); + } + } catch (Exception e) { + log.error("Error extracting SAML assertion: " + e.getMessage()); + challenge = new AuthChallenge() { + @Override + public boolean challenge(HttpFacade exchange) { + SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE); + exchange.getRequest().setError(error); + exchange.getResponse().sendError(403); + return true; + } + + @Override + public int getResponseCode() { + return 403; + } + }; + } + + SubjectType subject = assertion.getSubject(); + SubjectType.STSubType subType = subject.getSubType(); + NameIDType subjectNameID = (NameIDType) subType.getBaseID(); + String principalName = subjectNameID.getValue(); + + final Set roles = new HashSet<>(); + MultivaluedHashMap attributes = new MultivaluedHashMap<>(); + MultivaluedHashMap friendlyAttributes = new MultivaluedHashMap<>(); + + Set statements = assertion.getStatements(); + for (StatementAbstractType statement : statements) { + if (statement instanceof AttributeStatementType) { + AttributeStatementType attributeStatement = (AttributeStatementType) statement; + List attList = attributeStatement.getAttributes(); + for (AttributeStatementType.ASTChoiceType obj : attList) { + AttributeType attr = obj.getAttribute(); + if (isRole(attr)) { + List attributeValues = attr.getAttributeValue(); + if (attributeValues != null) { + for (Object attrValue : attributeValues) { + String role = getAttributeValue(attrValue); + log.debugv("Add role: {0}", role); + roles.add(role); + } + } + } else { + List attributeValues = attr.getAttributeValue(); + if (attributeValues != null) { + for (Object attrValue : attributeValues) { + String value = getAttributeValue(attrValue); + if (attr.getName() != null) { + attributes.add(attr.getName(), value); + } + if (attr.getFriendlyName() != null) { + friendlyAttributes.add(attr.getFriendlyName(), value); + } + } + } + } + + } + } + } + if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) { + if (deployment.getPrincipalAttributeName() != null) { + String attribute = attributes.getFirst(deployment.getPrincipalAttributeName()); + if (attribute != null) principalName = attribute; + else { + attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName()); + if (attribute != null) principalName = attribute; + } + } + } + + AuthnStatementType authn = null; + for (Object statement : assertion.getStatements()) { + if (statement instanceof AuthnStatementType) { + authn = (AuthnStatementType) statement; + break; + } + } + + + URI nameFormat = 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(); + final String sessionIndex = index; + SamlSession account = new SamlSession(principal, roles, sessionIndex); + sessionStore.saveAccount(account); + onCreateSession.onSessionCreated(account); + + // redirect to original request, it will be restored + String redirectUri = sessionStore.getRedirectUri(); + if (redirectUri != null) { + facade.getResponse().setHeader("Location", redirectUri); + facade.getResponse().setStatus(302); + facade.getResponse().end(); + } else { + log.debug("IDP initiated invocation"); + } + log.debug("AUTHENTICATED authn"); + + return AuthOutcome.AUTHENTICATED; + } + + private String getAttributeValue(Object attrValue) { + String value = null; + if (attrValue instanceof String) { + value = (String) attrValue; + } else if (attrValue instanceof Node) { + Node roleNode = (Node) attrValue; + value = roleNode.getFirstChild().getNodeValue(); + } else if (attrValue instanceof NameIDType) { + NameIDType nameIdType = (NameIDType) attrValue; + value = nameIdType.getValue(); + } else { + log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName()); + } + return value; + } + + protected boolean isRole(AttributeType attribute) { + return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName())); + } + + protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) { + boolean loggedIn = sessionStore.isLoggedIn(); + if (!loggedIn || !"logout".equals(relayState)) { + return AuthOutcome.NOT_ATTEMPTED; + } + sessionStore.logoutAccount(); + return AuthOutcome.LOGGED_OUT; + } + + protected SAMLDocumentHolder extractRedirectBindingResponse(String response) { + return SAMLRequestParser.parseRequestRedirectBinding(response); + } + + + protected SAMLDocumentHolder extractPostBindingResponse(String response) { + byte[] samlBytes = PostBindingUtil.base64Decode(response); + return SAMLRequestParser.parseResponseDocument(samlBytes); + } + + + protected AuthOutcome initiateLogin() { + challenge = createChallenge(); + return AuthOutcome.NOT_ATTEMPTED; + } + + protected AbstractInitiateLogin createChallenge() { + return new AbstractInitiateLogin(deployment, sessionStore) { + @Override + protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException { + Document document = authnRequestBuilder.toDocument(); + SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding(); + SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding); + } + }; + } + + protected boolean verifySSL() { + if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { + log.warn("SSL is required to authenticate"); + return true; + } + return false; + } + + public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException { + SAML2Signature saml2Signature = new SAML2Signature(); + try { + if (!saml2Signature.validate(document, publicKey)) { + throw new VerificationException("Invalid signature on document"); + } + } catch (ProcessingException e) { + throw new VerificationException("Error validating signature", e); + } + } + + public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException { + String request = facade.getRequest().getQueryParamValue(paramKey); + String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); + String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY); + String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); + + if (request == null) { + throw new VerificationException("SAML Request was null"); + } + if (algorithm == null) throw new VerificationException("SigAlg was null"); + if (signature == null) throw new VerificationException("Signature was null"); + + // Shibboleth doesn't sign the document for redirect binding. + // todo maybe a flag? + + String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE); + KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/") + .queryParam(paramKey, request); + if (relayState != null) { + builder.queryParam(GeneralConstants.RELAY_STATE, relayState); + } + builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm); + String rawQuery = builder.build().getRawQuery(); + + try { + //byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature); + byte[] decodedSignature = Base64.decode(signature); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm); + Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg + validator.initVerify(publicKey); + validator.update(rawQuery.getBytes("UTF-8")); + if (!validator.verify(decodedSignature)) { + throw new VerificationException("Invalid query param signature"); + } + } catch (Exception e) { + throw new VerificationException(e); + } + } +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java new file mode 100644 index 0000000000..4f499c740a --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java @@ -0,0 +1,13 @@ +package org.keycloak.adapters.saml.profile; + +import org.keycloak.adapters.saml.OnSessionCreated; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthOutcome; + +/** + * @author Pedro Igor + */ +public interface SamlAuthenticationHandler { + AuthOutcome handle(OnSessionCreated onCreateSession); + AuthChallenge getChallenge(); +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java new file mode 100644 index 0000000000..1155b0d45e --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java @@ -0,0 +1,37 @@ +package org.keycloak.adapters.saml.profile; + +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.spi.HttpFacade; + +/** + * @author Pedro Igor + */ +public class SamlInvocationContext { + + private String samlRequest; + private String samlResponse; + private String relayState; + + public SamlInvocationContext() { + this(null, null, null); + } + + public SamlInvocationContext(String samlRequest, String samlResponse, String relayState) { + this.samlRequest = samlRequest; + this.samlResponse = samlResponse; + this.relayState = relayState; + } + + public String getSamlRequest() { + return this.samlRequest; + } + + public String getSamlResponse() { + return this.samlResponse; + } + + public String getRelayState() { + return this.relayState; + } +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java new file mode 100644 index 0000000000..5fa99a0909 --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java @@ -0,0 +1,146 @@ +package org.keycloak.adapters.saml.profile.ecp; + +import org.keycloak.adapters.saml.AbstractInitiateLogin; +import org.keycloak.adapters.saml.OnSessionCreated; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.saml.profile.AbstractSamlAuthenticationHandler; +import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; +import org.keycloak.adapters.saml.profile.SamlInvocationContext; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.saml.BaseSAML2BindingBuilder; +import org.keycloak.saml.SAML2AuthnRequestBuilder; +import org.keycloak.saml.common.constants.JBossSAMLConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil; +import org.keycloak.saml.processing.web.util.PostBindingUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import javax.xml.soap.MessageFactory; +import javax.xml.soap.SOAPBody; +import javax.xml.soap.SOAPEnvelope; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPHeader; +import javax.xml.soap.SOAPHeaderElement; +import javax.xml.soap.SOAPMessage; + +/** + * @author Pedro Igor + */ +public class EcpAuthenticationHandler extends AbstractSamlAuthenticationHandler { + + public static final String PAOS_HEADER = "PAOS"; + public static final String PAOS_CONTENT_TYPE = "application/vnd.paos+xml"; + private static final String NS_PREFIX_PROFILE_ECP = "ecp"; + private static final String NS_PREFIX_SAML_PROTOCOL = "samlp"; + private static final String NS_PREFIX_SAML_ASSERTION = "saml"; + private static final String NS_PREFIX_PAOS_BINDING = "paos"; + + public static boolean canHandle(HttpFacade httpFacade) { + HttpFacade.Request request = httpFacade.getRequest(); + String acceptHeader = request.getHeader("Accept"); + String contentTypeHeader = request.getHeader("Content-Type"); + + return (acceptHeader != null && acceptHeader.contains(PAOS_CONTENT_TYPE) && request.getHeader(PAOS_HEADER) != null) + || (contentTypeHeader != null && contentTypeHeader.contains(PAOS_CONTENT_TYPE)); + } + + public static SamlAuthenticationHandler create(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + return new EcpAuthenticationHandler(facade, deployment, sessionStore); + } + + private EcpAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + super(facade, deployment, sessionStore); + } + + @Override + protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) { + throw new RuntimeException("Not supported."); + } + + + @Override + public AuthOutcome handle(OnSessionCreated onCreateSession) { + String header = facade.getRequest().getHeader(PAOS_HEADER); + + if (header != null) { + return doHandle(new SamlInvocationContext(), onCreateSession); + } else { + try { + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage soapMessage = messageFactory.createMessage(null, facade.getRequest().getInputStream()); + SOAPBody soapBody = soapMessage.getSOAPBody(); + Node authnRequestNode = soapBody.getFirstChild(); + Document document = DocumentUtil.createDocument(); + + document.appendChild(document.importNode(authnRequestNode, true)); + + String samlResponse = PostBindingUtil.base64Encode(DocumentUtil.asString(document)); + + return doHandle(new SamlInvocationContext(null, samlResponse, null), onCreateSession); + } catch (Exception e) { + throw new RuntimeException("Error creating fault message.", e); + } + } + } + + @Override + protected AbstractInitiateLogin createChallenge() { + return new AbstractInitiateLogin(deployment, sessionStore) { + @Override + protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) { + try { + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage message = messageFactory.createMessage(); + + SOAPEnvelope envelope = message.getSOAPPart().getEnvelope(); + + envelope.addNamespaceDeclaration(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get()); + envelope.addNamespaceDeclaration(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get()); + envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get()); + envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get()); + + createPaosRequestHeader(envelope); + createEcpRequestHeader(envelope); + + SOAPBody body = envelope.getBody(); + + body.addDocument(binding.postBinding(authnRequestBuilder.toDocument()).getDocument()); + + message.writeTo(httpFacade.getResponse().getOutputStream()); + } catch (Exception e) { + throw new RuntimeException("Could not create AuthnRequest.", e); + } + } + + private void createEcpRequestHeader(SOAPEnvelope envelope) throws SOAPException { + SOAPHeader headers = envelope.getHeader(); + SOAPHeaderElement ecpRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PROFILE_ECP)); + + ecpRequestHeader.setMustUnderstand(true); + ecpRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next"); + ecpRequestHeader.addAttribute(envelope.createName("ProviderName"), deployment.getEntityID()); + ecpRequestHeader.addAttribute(envelope.createName("IsPassive"), "0"); + ecpRequestHeader.addChildElement(envelope.createQName("Issuer", "saml")).setValue(deployment.getEntityID()); + ecpRequestHeader.addChildElement(envelope.createQName("IDPList", "samlp")) + .addChildElement(envelope.createQName("IDPEntry", "samlp")) + .addAttribute(envelope.createName("ProviderID"), deployment.getIDP().getEntityID()) + .addAttribute(envelope.createName("Name"), deployment.getIDP().getEntityID()) + .addAttribute(envelope.createName("Loc"), deployment.getIDP().getSingleSignOnService().getRequestBindingUrl()); + } + + private void createPaosRequestHeader(SOAPEnvelope envelope) throws SOAPException { + SOAPHeader headers = envelope.getHeader(); + SOAPHeaderElement paosRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PAOS_BINDING)); + + paosRequestHeader.setMustUnderstand(true); + paosRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next"); + paosRequestHeader.addAttribute(envelope.createName("service"), JBossSAMLURIConstants.ECP_PROFILE.get()); + paosRequestHeader.addAttribute(envelope.createName("responseConsumerURL"), deployment.getAssertionConsumerServiceUrl()); + } + }; + } +} \ No newline at end of file diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java new file mode 100644 index 0000000000..f3e98e5492 --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java @@ -0,0 +1,111 @@ +package org.keycloak.adapters.saml.profile.webbrowsersso; + +import org.keycloak.adapters.saml.OnSessionCreated; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.saml.SamlUtil; +import org.keycloak.adapters.saml.profile.AbstractSamlAuthenticationHandler; +import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; +import org.keycloak.adapters.saml.profile.SamlInvocationContext; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.saml.BaseSAML2BindingBuilder; +import org.keycloak.saml.SAML2LogoutRequestBuilder; +import org.keycloak.saml.SAML2LogoutResponseBuilder; +import org.keycloak.saml.common.constants.GeneralConstants; + +/** + * @author Pedro Igor + */ +public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticationHandler { + + public static SamlAuthenticationHandler create(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + return new WebBrowserSsoAuthenticationHandler(facade, deployment, sessionStore); + } + + private WebBrowserSsoAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + super(facade, deployment, sessionStore); + } + + @Override + public AuthOutcome handle(OnSessionCreated onCreateSession) { + return doHandle(new SamlInvocationContext(facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY), + facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY), + facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE)), onCreateSession); + } + + @Override + protected AuthOutcome handleRequest() { + boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO")); + + if (globalLogout) { + return globalLogout(); + } + + return AuthOutcome.AUTHENTICATED; + } + + @Override + protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) { + if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) { + sessionStore.logoutByPrincipal(request.getNameID().getValue()); + } else { + sessionStore.logoutBySsoId(request.getSessionIndex()); + } + + String issuerURL = deployment.getEntityID(); + SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder(); + builder.logoutRequestID(request.getID()); + builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl()); + builder.issuer(issuerURL); + BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState); + if (deployment.getIDP().getSingleLogoutService().signResponse()) { + binding.signatureAlgorithm(deployment.getSignatureAlgorithm()) + .signWith(deployment.getSigningKeyPair()) + .signDocument(); + if (deployment.getSignatureCanonicalizationMethod() != null) + binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod()); + } + + + try { + SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(), + deployment.getIDP().getSingleLogoutService().getResponseBinding()); + } catch (Exception e) { + log.error("Could not send logout response SAML request", e); + return AuthOutcome.FAILED; + } + return AuthOutcome.NOT_ATTEMPTED; + } + + private AuthOutcome globalLogout() { + SamlSession account = sessionStore.getAccount(); + if (account == null) { + return AuthOutcome.NOT_ATTEMPTED; + } + SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() + .assertionExpiration(30) + .issuer(deployment.getEntityID()) + .sessionIndex(account.getSessionIndex()) + .userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat()) + .destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl()); + BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder(); + if (deployment.getIDP().getSingleLogoutService().signRequest()) { + binding.signWith(deployment.getSigningKeyPair()) + .signDocument(); + } + + binding.relayState("logout"); + + try { + SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding()); + sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT); + } catch (Exception e) { + log.error("Could not send global logout SAML request", e); + return AuthOutcome.FAILED; + } + return AuthOutcome.NOT_ATTEMPTED; + } +} diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java index fb90e17922..219042b24f 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java @@ -65,7 +65,8 @@ public enum JBossSAMLConstants { "XACMLAuthzDecisionQuery"), XACML_AUTHZ_DECISION_QUERY_TYPE("XACMLAuthzDecisionQueryType"), XACML_AUTHZ_DECISION_STATEMENT_TYPE( "XACMLAuthzDecisionStatementType"), HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), ONE_TIME_USE ("OneTimeUse"), UNSOLICITED_RESPONSE_TARGET("TARGET"), UNSOLICITED_RESPONSE_SAML_VERSION("SAML_VERSION"), UNSOLICITED_RESPONSE_SAML_BINDING("SAML_BINDING"), - ROLE_DESCRIPTOR("RoleDescriptor"); + ROLE_DESCRIPTOR("RoleDescriptor"), + REQUEST_AUTHENTICATED("RequestAuthenticated"); private String name; diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java index 3833c56f38..ad7bee5656 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java @@ -73,12 +73,15 @@ public enum JBossSAMLURIConstants { "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"), PROTOCOL_NSURI("urn:oasis:names:tc:SAML:2.0:protocol"), + ECP_PROFILE("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"), + PAOS_BINDING("urn:liberty:paos:2003-08"), SIGNATURE_DSA_SHA1("http://www.w3.org/2000/09/xmldsig#dsa-sha1"), SIGNATURE_RSA_SHA1( "http://www.w3.org/2000/09/xmldsig#rsa-sha1"), - SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), SAML_HTTP_REDIRECT_BINDING( - "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"), + SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), + SAML_HTTP_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"), + SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"), SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"), diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 0bc3edeea0..07c528ddd9 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -84,6 +84,7 @@ public class SamlProtocol implements LoginProtocol { public static final String SAML_BINDING = "saml_binding"; public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login"; public static final String SAML_POST_BINDING = "post"; + public static final String SAML_SOAP_BINDING = "soap"; public static final String SAML_REDIRECT_BINDING = "get"; public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; @@ -165,11 +166,7 @@ public class SamlProtocol implements LoginProtocol { try { JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); Document document = builder.buildDocument(); - if (isPostBinding(clientSession)) { - return binding.postBinding(document).response(clientSession.getRedirectUri()); - } else { - return binding.redirectBinding(document).response(clientSession.getRedirectUri()); - } + return buildErrorResponse(clientSession, binding, document); } catch (Exception e) { return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); } @@ -180,6 +177,14 @@ public class SamlProtocol implements LoginProtocol { } } + protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { + if (isPostBinding(clientSession)) { + return binding.postBinding(document).response(clientSession.getRedirectUri()); + } else { + return binding.redirectBinding(document).response(clientSession.getRedirectUri()); + } + } + private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) { switch (error) { case CANCELLED_BY_USER: @@ -390,17 +395,21 @@ public class SamlProtocol implements LoginProtocol { bindingBuilder.encrypt(publicKey); } try { - if (isPostBinding(clientSession)) { - return bindingBuilder.postBinding(samlDocument).response(redirectUri); - } else { - return bindingBuilder.redirectBinding(samlDocument).response(redirectUri); - } + return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder); } catch (Exception e) { logger.error("failed", e); return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); } } + protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { + if (isPostBinding(clientSession)) { + return bindingBuilder.postBinding(samlDocument).response(redirectUri); + } else { + return bindingBuilder.redirectBinding(samlDocument).response(redirectUri); + } + } + public static boolean requiresRealmSignature(ClientModel client) { return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE)); } @@ -544,11 +553,7 @@ public class SamlProtocol implements LoginProtocol { } try { - if (isLogoutPostBindingForInitiator(userSession)) { - return binding.postBinding(builder.buildDocument()).response(logoutBindingUri); - } else { - return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri); - } + return buildLogoutResponse(userSession, logoutBindingUri, builder, binding); } catch (ConfigurationException e) { throw new RuntimeException(e); } catch (ProcessingException e) { @@ -558,6 +563,14 @@ public class SamlProtocol implements LoginProtocol { } } + protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException { + if (isLogoutPostBindingForInitiator(userSession)) { + return binding.postBinding(builder.buildDocument()).response(logoutBindingUri); + } else { + return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri); + } + } + @Override public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java index a7a86edc3a..7dcc86695b 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java @@ -42,7 +42,7 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { @Override public String getId() { - return "saml"; + return SamlProtocol.LOGIN_PROTOCOL; } @Override @@ -90,8 +90,9 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { @Override protected void addDefaults(ClientModel client) { - for (ProtocolMapperModel model : defaultBuiltins) client.addProtocolMapper(model); - + for (ProtocolMapperModel model : defaultBuiltins) { + model.setProtocol(getId()); + client.addProtocolMapper(model); + } } - } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 34025933d1..f9aa30b148 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -16,6 +16,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.VerificationException; import org.keycloak.common.util.StreamUtil; import org.keycloak.dom.saml.v2.SAML2Object; @@ -34,7 +35,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService; import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAMLRequestParser; import org.keycloak.saml.SignatureAlgorithm; @@ -221,7 +224,7 @@ public class SamlService extends AuthorizationEndpointBase { } ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); + clientSession.setAuthMethod(getLoginProtocol()); clientSession.setRedirectUri(redirect); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); @@ -246,7 +249,7 @@ public class SamlService extends AuthorizationEndpointBase { return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive()); } - private String getBindingType(AuthnRequestType requestAbstractType) { + protected String getBindingType(AuthnRequestType requestAbstractType) { URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); if (requestedProtocolBinding != null) { @@ -370,7 +373,7 @@ public class SamlService extends AuthorizationEndpointBase { } } - protected class PostBindingProtocol extends BindingProtocol { + public class PostBindingProtocol extends BindingProtocol { @Override protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException { @@ -443,7 +446,12 @@ public class SamlService extends AuthorizationEndpointBase { } protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) { - return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive); + LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); + protocol.setRealm(realm) + .setHttpHeaders(request.getHttpHeaders()) + .setUriInfo(uriInfo) + .setEventBuilder(event); + return handleBrowserAuthenticationRequest(clientSession, protocol, isPassive); } /** @@ -463,6 +471,16 @@ public class SamlService extends AuthorizationEndpointBase { return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState); } + @POST + @Consumes("application/soap+xml") + public Response soapBinding(InputStream inputStream) { + SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, authManager); + + ResteasyProviderFactory.getInstance().injectProperties(bindingService); + + return bindingService.authenticate(inputStream); + } + @GET @Path("descriptor") @Produces(MediaType.APPLICATION_XML) @@ -519,7 +537,7 @@ public class SamlService extends AuthorizationEndpointBase { } ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); + clientSession.setAuthMethod(getLoginProtocol()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING); @@ -537,4 +555,8 @@ public class SamlService extends AuthorizationEndpointBase { } + protected String getLoginProtocol() { + return SamlProtocol.LOGIN_PROTOCOL; + } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java new file mode 100644 index 0000000000..dba1b295b9 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java @@ -0,0 +1,109 @@ +package org.keycloak.protocol.saml.profile.ecp; + +import org.keycloak.Config; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.protocol.saml.SamlProtocolFactory; +import org.keycloak.protocol.saml.profile.ecp.util.Soap; +import org.keycloak.protocol.saml.profile.ecp.util.Soap.SoapMessageBuilder; +import org.keycloak.saml.SAML2LogoutResponseBuilder; +import org.keycloak.saml.common.constants.JBossSAMLConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.services.managers.AuthenticationManager; +import org.w3c.dom.Document; + +import javax.ws.rs.core.Response; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPHeaderElement; +import java.io.IOException; + +/** + * @author Pedro Igor + */ +public class SamlEcpProfileProtocolFactory extends SamlProtocolFactory { + + static final String ID = "saml-ecp-profile"; + + private static final String NS_PREFIX_PROFILE_ECP = "ecp"; + private static final String NS_PREFIX_SAML_PROTOCOL = "samlp"; + private static final String NS_PREFIX_SAML_ASSERTION = "saml"; + + @Override + public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { + return new SamlEcpProfileService(realm, event, authManager); + } + + @Override + public LoginProtocol create(KeycloakSession session) { + return new SamlProtocol() { + // method created to send a SOAP Binding response instead of a HTTP POST response + @Override + protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { + Document document = bindingBuilder.postBinding(samlDocument).getDocument(); + + try { + SoapMessageBuilder messageBuilder = Soap.createMessage() + .addNamespace(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get()) + .addNamespace(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get()) + .addNamespace(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get()); + + createEcpResponseHeader(redirectUri, messageBuilder); + createRequestAuthenticatedHeader(clientSession, messageBuilder); + + messageBuilder.addToBody(document); + + return messageBuilder.build(); + } catch (Exception e) { + throw new RuntimeException("Error while creating SAML response.", e); + } + } + + private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, SoapMessageBuilder messageBuilder) { + ClientModel client = clientSession.getClient(); + + if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { + SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP); + + ecpRequestAuthenticated.setMustUnderstand(true); + ecpRequestAuthenticated.setActor("http://schemas.xmlsoap.org/soap/actor/next"); + } + } + + private void createEcpResponseHeader(String redirectUri, SoapMessageBuilder messageBuilder) throws SOAPException { + SOAPHeaderElement ecpResponseHeader = messageBuilder.addHeader(JBossSAMLConstants.RESPONSE.get(), NS_PREFIX_PROFILE_ECP); + + ecpResponseHeader.setMustUnderstand(true); + ecpResponseHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next"); + ecpResponseHeader.addAttribute(messageBuilder.createName(JBossSAMLConstants.ASSERTION_CONSUMER_SERVICE_URL.get()), redirectUri); + } + + @Override + protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { + return Soap.createMessage().addToBody(document).build(); + } + + @Override + protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException { + return Soap.createFault().reason("Logout not supported.").build(); + } + }.setSession(session); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public String getId() { + return ID; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java new file mode 100644 index 0000000000..c16b997757 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java @@ -0,0 +1,70 @@ +package org.keycloak.protocol.saml.profile.ecp; + +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.protocol.saml.SamlService; +import org.keycloak.protocol.saml.profile.ecp.util.Soap; +import org.keycloak.services.managers.AuthenticationManager; + +import javax.ws.rs.core.Response; +import java.io.InputStream; + +/** + * @author Pedro Igor + */ +public class SamlEcpProfileService extends SamlService { + + public SamlEcpProfileService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { + super(realm, event, authManager); + } + + public Response authenticate(InputStream inputStream) { + try { + return new PostBindingProtocol() { + @Override + protected String getBindingType(AuthnRequestType requestAbstractType) { + return SamlProtocol.SAML_SOAP_BINDING; + } + + @Override + protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) { + // force passive authentication when executing this profile + requestAbstractType.setIsPassive(true); + requestAbstractType.setDestination(uriInfo.getAbsolutePath()); + return super.loginRequest(relayState, requestAbstractType, client); + } + }.execute(Soap.toSamlHttpPostMessage(inputStream), null, null); + } catch (Exception e) { + String reason = "Some error occurred while processing the AuthnRequest."; + String detail = e.getMessage(); + + if (detail == null) { + detail = reason; + } + + return Soap.createFault().reason(reason).detail(detail).build(); + } + } + + @Override + protected String getLoginProtocol() { + return SamlEcpProfileProtocolFactory.ID; + } + + @Override + protected AuthenticationFlowModel getAuthenticationFlow() { + for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) { + if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) { + return flowModel; + } + } + + throw new RuntimeException("Could not resolve authentication flow for SAML ECP Profile."); + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java new file mode 100644 index 0000000000..2e1550229b --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java @@ -0,0 +1,174 @@ +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.*; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.CredentialRepresentation; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class HttpBasicAuthenticator implements AuthenticatorFactory { + + public static final String PROVIDER_ID = "http-basic-authenticator"; + + @Override + public String getDisplayType() { + return null; + } + + @Override + public String getReferenceCategory() { + return null; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public Requirement[] getRequirementChoices() { + return new Requirement[0]; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return null; + } + + @Override + public List getConfigProperties() { + 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().users().validCredentials(context.getSession(), realm, user, UserCredentialModel.password(password)); + + if (valid) { + context.getClientSession().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()); + } + } + } + } + + private String[] getUsernameAndPassword(HttpHeaders httpHeaders) { + List authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION); + + 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) { + + } + + @Override + public void close() { + + } + }; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java new file mode 100644 index 0000000000..4bdf76a10e --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java @@ -0,0 +1,177 @@ +package org.keycloak.protocol.saml.profile.ecp.util; + +import org.keycloak.saml.common.constants.JBossSAMLConstants; +import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil; +import org.keycloak.saml.processing.web.util.PostBindingUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.xml.soap.MessageFactory; +import javax.xml.soap.Name; +import javax.xml.soap.SOAPBody; +import javax.xml.soap.SOAPEnvelope; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPFault; +import javax.xml.soap.SOAPHeaderElement; +import javax.xml.soap.SOAPMessage; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Locale; + +/** + * @author Pedro Igor + */ +public final class Soap { + + public static SoapFaultBuilder createFault() { + return new SoapFaultBuilder(); + } + + public static SoapMessageBuilder createMessage() { + return new SoapMessageBuilder(); + } + + /** + *

Returns a string encoded accordingly with the SAML HTTP POST Binding specification based on the + * given inputStream which must contain a valid SOAP message. + * + *

The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message. + * + * @param inputStream the input stream containing a valid SOAP message with a Body that contains a SAML message + * + * @return a string encoded accordingly with the SAML HTTP POST Binding specification + */ + public static String toSamlHttpPostMessage(InputStream inputStream) { + try { + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage soapMessage = messageFactory.createMessage(null, inputStream); + SOAPBody soapBody = soapMessage.getSOAPBody(); + Node authnRequestNode = soapBody.getFirstChild(); + Document document = DocumentUtil.createDocument(); + + document.appendChild(document.importNode(authnRequestNode, true)); + + return PostBindingUtil.base64Encode(DocumentUtil.asString(document)); + } catch (Exception e) { + throw new RuntimeException("Error creating fault message.", e); + } + } + + public static class SoapMessageBuilder { + private final SOAPMessage message; + private final SOAPBody body; + private final SOAPEnvelope envelope; + + private SoapMessageBuilder() { + try { + this.message = MessageFactory.newInstance().createMessage(); + this.envelope = message.getSOAPPart().getEnvelope(); + this.body = message.getSOAPBody(); + } catch (Exception e) { + throw new RuntimeException("Error creating fault message.", e); + } + } + + public SoapMessageBuilder addToBody(Document document) { + try { + this.body.addDocument(document); + } catch (SOAPException e) { + throw new RuntimeException("Could not add document to SOAP body.", e); + } + return this; + } + + public SoapMessageBuilder addNamespace(String prefix, String ns) { + try { + envelope.addNamespaceDeclaration(prefix, ns); + } catch (SOAPException e) { + throw new RuntimeException("Could not add namespace to SOAP Envelope.", e); + } + return this; + } + + public SOAPHeaderElement addHeader(String name, String prefix) { + try { + return this.envelope.getHeader().addHeaderElement(envelope.createQName(name, prefix)); + } catch (SOAPException e) { + throw new RuntimeException("Could not add SOAP Header.", e); + } + } + + public Name createName(String name) { + try { + return this.envelope.createName(name); + } catch (SOAPException e) { + throw new RuntimeException("Could not create Name.", e); + } + } + + public Response build() { + return build(Status.OK); + } + + Response build(Status status) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + try { + this.message.writeTo(outputStream); + } catch (Exception e) { + throw new RuntimeException("Error while building SOAP Fault.", e); + } + + return Response.status(status).entity(outputStream.toByteArray()).build(); + } + + SOAPMessage getMessage() { + return this.message; + } + } + + public static class SoapFaultBuilder { + + private final SOAPFault fault; + private final SoapMessageBuilder messageBuilder; + + private SoapFaultBuilder() { + this.messageBuilder = createMessage(); + try { + this.fault = messageBuilder.getMessage().getSOAPBody().addFault(); + } catch (SOAPException e) { + throw new RuntimeException("Could not create SOAP Fault.", e); + } + } + + public SoapFaultBuilder detail(String detail) { + try { + this.fault.addDetail().setValue(detail); + } catch (SOAPException e) { + throw new RuntimeException("Error creating fault message.", e); + } + return this; + } + + public SoapFaultBuilder reason(String reason) { + try { + this.fault.setFaultString(reason); + } catch (SOAPException e) { + throw new RuntimeException("Error creating fault message.", e); + } + return this; + } + + public SoapFaultBuilder code(String code) { + try { + this.fault.setFaultCode(code); + } catch (SOAPException e) { + throw new RuntimeException("Error creating fault message.", e); + } + return this; + } + + public Response build() { + return this.messageBuilder.build(Status.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100755 index 0000000000..9ac8020231 --- /dev/null +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1 @@ +org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator \ No newline at end of file diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory index d0a2dd046f..ae434f63eb 100755 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory @@ -1 +1,2 @@ -org.keycloak.protocol.saml.SamlProtocolFactory \ No newline at end of file +org.keycloak.protocol.saml.SamlProtocolFactory +org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileProtocolFactory \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index a1fc4a7708..9dc5548f57 100644 --- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -87,7 +87,7 @@ public abstract class AuthorizationEndpointBase { } } - AuthenticationFlowModel flow = realm.getBrowserFlow(); + AuthenticationFlowModel flow = getAuthenticationFlow(); String flowId = flow.getId(); AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH); @@ -127,6 +127,10 @@ public abstract class AuthorizationEndpointBase { } } + protected AuthenticationFlowModel getAuthenticationFlow() { + return realm.getBrowserFlow(); + } + protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) { logger.debug("Automatically redirect to identity provider: " + providerId); return Response.temporaryRedirect( diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java new file mode 100755 index 0000000000..c02deeb3b9 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java @@ -0,0 +1,230 @@ +package org.keycloak.testsuite.saml; + +import org.jboss.resteasy.util.Base64; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusCodeType; +import org.keycloak.dom.saml.v2.protocol.StatusResponseType; +import org.keycloak.saml.common.constants.JBossSAMLConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import org.keycloak.testsuite.samlfilter.SamlAdapterTest; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import javax.xml.namespace.QName; +import javax.xml.soap.MessageFactory; +import javax.xml.soap.SOAPHeader; +import javax.xml.soap.SOAPHeaderElement; +import javax.xml.soap.SOAPMessage; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Iterator; +import java.util.Map; + +import static javax.ws.rs.core.Response.Status.OK; +import static org.junit.Assert.*; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlEcpProfileTest { + + protected String APP_SERVER_BASE_URL = "http://localhost:8081"; + + @ClassRule + public static org.keycloak.testsuite.samlfilter.SamlKeycloakRule keycloakRule = new org.keycloak.testsuite.samlfilter.SamlKeycloakRule() { + @Override + public void initWars() { + ClassLoader classLoader = SamlAdapterTest.class.getClassLoader(); + + initializeSamlSecuredWar("/keycloak-saml/ecp/ecp-sp", "/ecp-sp", "ecp-sp.war", classLoader); + } + + @Override + public String getRealmJson() { + return "/keycloak-saml/ecp/testsamlecp.json"; + } + }; + + @Test + public void testSuccessfulEcpFlow() throws Exception { + Response authnRequestResponse = ClientBuilder.newClient().target(APP_SERVER_BASE_URL + "/ecp-sp/").request() + .header("Accept", "text/html; application/vnd.paos+xml") + .header("PAOS", "ver='urn:liberty:paos:2003-08' ;'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'") + .get(); + + SOAPMessage authnRequestMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authnRequestResponse.readEntity(byte[].class))); + + printDocument(authnRequestMessage.getSOAPPart().getContent(), System.out); + + Iterator it = authnRequestMessage.getSOAPHeader().getChildElements(new QName("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", "Request")); + SOAPHeaderElement ecpRequestHeader = it.next(); + NodeList idpList = ecpRequestHeader.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", "IDPList"); + + assertEquals("No IDPList returned from Service Provider", 1, idpList.getLength()); + + NodeList idpEntries = idpList.item(0).getChildNodes(); + + assertEquals("No IDPEntry returned from Service Provider", 1, idpEntries.getLength()); + + String singleSignOnService = null; + + for (int i = 0; i < idpEntries.getLength(); i++) { + Node item = idpEntries.item(i); + NamedNodeMap attributes = item.getAttributes(); + Node location = attributes.getNamedItem("Loc"); + + singleSignOnService = location.getNodeValue(); + } + + assertNotNull("Could not obtain SSO Service URL", singleSignOnService); + + Document authenticationRequest = authnRequestMessage.getSOAPBody().getFirstChild().getOwnerDocument(); + String username = "pedroigor"; + String password = "password"; + String pair = username + ":" + password; + String authHeader = "Basic " + new String(Base64.encodeBytes(pair.getBytes())); + + Response authenticationResponse = ClientBuilder.newClient().target(singleSignOnService).request() + .header(HttpHeaders.AUTHORIZATION, authHeader) + .post(Entity.entity(DocumentUtil.asString(authenticationRequest), "application/soap+xml")); + + assertEquals(OK.getStatusCode(), authenticationResponse.getStatus()); + + SOAPMessage responseMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authenticationResponse.readEntity(byte[].class))); + + printDocument(responseMessage.getSOAPPart().getContent(), System.out); + + SOAPHeader responseMessageHeaders = responseMessage.getSOAPHeader(); + + NodeList ecpResponse = responseMessageHeaders.getElementsByTagNameNS(JBossSAMLURIConstants.ECP_PROFILE.get(), JBossSAMLConstants.RESPONSE.get()); + + assertEquals("No ECP Response", 1, ecpResponse.getLength()); + + Node samlResponse = responseMessage.getSOAPBody().getFirstChild(); + + assertNotNull(samlResponse); + + ResponseType responseType = (ResponseType) new SAMLParser().parse(DocumentUtil.getNodeAsStream(samlResponse)); + StatusCodeType statusCode = responseType.getStatus().getStatusCode(); + + assertEquals(statusCode.getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get()); + assertEquals("http://localhost:8081/ecp-sp/", responseType.getDestination()); + assertNotNull(responseType.getSignature()); + assertEquals(1, responseType.getAssertions().size()); + + SOAPMessage samlResponseRequest = MessageFactory.newInstance().createMessage(); + + samlResponseRequest.getSOAPBody().addDocument(responseMessage.getSOAPBody().extractContentAsDocument()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + samlResponseRequest.writeTo(os); + + Response serviceProviderFinalResponse = ClientBuilder.newClient().target(responseType.getDestination()).request() + .post(Entity.entity(os.toByteArray(), "application/vnd.paos+xml")); + + Map cookies = serviceProviderFinalResponse.getCookies(); + + Builder resourceRequest = ClientBuilder.newClient().target(responseType.getDestination() + "/index.html").request(); + + for (NewCookie cookie : cookies.values()) { + resourceRequest.cookie(cookie); + } + + Response resourceResponse = resourceRequest.get(); + + assertTrue(resourceResponse.readEntity(String.class).contains("pedroigor")); + } + + @Test + public void testInvalidCredentials() throws Exception { + Response authnRequestResponse = ClientBuilder.newClient().target(APP_SERVER_BASE_URL + "/ecp-sp/").request() + .header("Accept", "text/html; application/vnd.paos+xml") + .header("PAOS", "ver='urn:liberty:paos:2003-08' ;'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'") + .get(); + + SOAPMessage authnRequestMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authnRequestResponse.readEntity(byte[].class))); + Iterator it = authnRequestMessage.getSOAPHeader().getChildElements(new QName("urn:liberty:paos:2003-08", "Request")); + + it.next(); + + it = authnRequestMessage.getSOAPHeader().getChildElements(new QName("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", "Request")); + SOAPHeaderElement ecpRequestHeader = it.next(); + NodeList idpList = ecpRequestHeader.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", "IDPList"); + + assertEquals("No IDPList returned from Service Provider", 1, idpList.getLength()); + + NodeList idpEntries = idpList.item(0).getChildNodes(); + + assertEquals("No IDPEntry returned from Service Provider", 1, idpEntries.getLength()); + + String singleSignOnService = null; + + for (int i = 0; i < idpEntries.getLength(); i++) { + Node item = idpEntries.item(i); + NamedNodeMap attributes = item.getAttributes(); + Node location = attributes.getNamedItem("Loc"); + + singleSignOnService = location.getNodeValue(); + } + + assertNotNull("Could not obtain SSO Service URL", singleSignOnService); + + Document authenticationRequest = authnRequestMessage.getSOAPBody().getFirstChild().getOwnerDocument(); + String username = "pedroigor"; + String password = "baspassword"; + String pair = username + ":" + password; + String authHeader = "Basic " + new String(Base64.encodeBytes(pair.getBytes())); + + Response authenticationResponse = ClientBuilder.newClient().target(singleSignOnService).request() + .header(HttpHeaders.AUTHORIZATION, authHeader) + .post(Entity.entity(DocumentUtil.asString(authenticationRequest), "application/soap+xml")); + + assertEquals(OK.getStatusCode(), authenticationResponse.getStatus()); + + SOAPMessage responseMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authenticationResponse.readEntity(byte[].class))); + Node samlResponse = responseMessage.getSOAPBody().getFirstChild(); + + assertNotNull(samlResponse); + + StatusResponseType responseType = (StatusResponseType) new SAMLParser().parse(DocumentUtil.getNodeAsStream(samlResponse)); + StatusCodeType statusCode = responseType.getStatus().getStatusCode(); + + assertNotEquals(statusCode.getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get()); + } + + public static void printDocument(Source doc, OutputStream out) throws IOException, TransformerException { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + + transformer.transform(doc, + new StreamResult(new OutputStreamWriter(out, "UTF-8"))); + } +} diff --git a/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..df39712bf3 --- /dev/null +++ b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks new file mode 100755 index 0000000000000000000000000000000000000000..144830bc77683d1d0a3d29f8793471d4f395bdb0 GIT binary patch literal 1705 zcmezO_TO6u1_mZ5W@J#!C@Cqh($~+)PfpCq$S*FjvM{hP&@WERNiEhb0P=N1M z7#KYz`B=UJb?!81V%loJ$Ht}2#>m2`#U#kc$jZRd#I(>@`_HHQr3*BE{x`hDyEHld z+K~ja7y5T|r=%}Fu;`1zi=v(re>&z=o_dkol)j+zYO>t=Qy1S}v3en%vP3563G;>m zf%$6H#y=LWo_E+v&g1u@xzTso16CaPbkO_D8o@;(`MobV^Igq3BNxZ5w|jA2W>>?O zsK)o5D^C2naN_4wlZOiFZx$ZYW2t4}JhWx=_GhgLGruICtx3+%d3V!9>Dt7nXAhlc z$;{X;mJr8NzU5%3ud}q*;Tc<&7_HRg&F#_Q?wWFKHOG|)@xdBe`K>+ek+V}K-){E) zl)cDAKzFrg_KD*rPhJ#0xY%188oeS%DBgct!6JRDwVn#urq)k%GX7O$-%BXGHp?M$ zPK(%~JsS=itGtnGNeXRIJS=enoqTH}>U?=&w0Rv-!V zt0IaI7=L{C`R*=P35_m^|6)DbnI1Rom?{^itz&v>PMs${YwWF4+mV{{ zB3p#_4CDJAHQ!e4#HZJ%dA%($R{WrMXd~~o9sJ4l4<@p_UZ&<;=%+j9w)>u?FXr2x z;Qi$;R(a*~n?JwFjfqj;bam5w#iu)2cStlBuRNH-DYCa*SFTj`Ruc2O%^{1v zS^p3Un$kT>@>nGEyG?KA#5`*A-Q)TDd5mCQN4nqL|I4L=R^IMCEM*}Xua~%r;rLwD z(rqehS=sAVMB2P;SIyh9w+fi%85vk3^h^ybfvg(_O^jE7*p`Wn(Np4IY>ELd8>d#A zN85K^Mn-N{1_J{_Jp&y!=1>-9Vd650ft)z6p^2e^p_!qPiHVVMlsK;ujB93K97TqS zjq{OR1z|$TGQU@sN$9wB)LN$MwU<2= zd)SlRE7$3UyG=O%*+B2uv-3&H4597J6WaBsZpb_&f7j)l$IXYTF6-xcsr5`yWnyMz zU_^EvFwmHR?&_X(ZsE4#TgmHFu2rXfU6s0)lCKe^<7iiUsaqK$zd9h>c57vO3iO-78%EoLL<8ZmZxNJeq&U-KBw5j(T{eI-t zj}(o0{2w&pRSxr|MK;+T;phADf{DX@d*|wohzX72{tZ3*&sbbC2N!KDDXF>n@KOsN z=~ICbKGC4@Hz>{}*<-lToNvHy$ZNn2iF2?C$Wdx)Vu&22rlzLAD8`N=&Wn#AY>fF{RdOu_iDQssmI(9hPO(2Uiv*4ZQlGJH!&z=`K zw%L^5bkqb1@@gO=+rT(BxQD(6^h&8PDm@hVJA%Qxx$H92KBf6;Q! zeu+xYdBwlV_8D(8vle0u7ZYR_ZJGO3A6Ja^r!9<;Jf^6B_V*`!pI4_%{2#46Qrcoa z{d4O3!wr>pceY+y88$ipliuM2X9G4KozS*BUZS)4a=%Gc^OV}R*}*rh8m{{;m0Oa) z^Ysn?xlP?KeF94gT3Fr`zuJ}MUVH6cw8xR2&A~UG*V~pyDQR4_ymoT_iPrx=pDW$^ gV7obl)$b?cUQd>i8HM*{tk@@=IXfvlri|w}0JG8182|tP literal 0 HcmV?d00001 diff --git a/testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json b/testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json new file mode 100755 index 0000000000..981cbda169 --- /dev/null +++ b/testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json @@ -0,0 +1,67 @@ +{ + "id": "demo", + "realm": "demo", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "resetPasswordAllowed": true, + "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" ], + "defaultRoles": [ "user" ], + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port":"3025" + }, + "users" : [ + { + "username" : "pedroigor", + "enabled": true, + "email" : "psilva@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "attributes" : { + "phone": "617" + }, + "realmRoles": ["manager", "user"] + } + ], + "applications": [ + { + "name": "http://localhost:8081/ecp-sp/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8081/ecp-sp", + "redirectUris": [ + "http://localhost:8081/ecp-sp/*" + ], + "attributes": { + "saml_assertion_consumer_url_post": "http://localhost:8081/ecp-sp/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/ecp-sp/", + "saml_single_logout_service_url_post": "http://localhost:8081/ecp-sp/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/ecp-sp/", + "saml.server.signature": "true", + "saml.signature.algorithm": "RSA_SHA256", + "saml.client.signature": "true", + "saml.authnstatement": "true", + "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" + } + } + ], + "roles" : { + "realm" : [ + { + "name": "manager", + "description": "Have Manager privileges" + }, + { + "name": "user", + "description": "Have User privileges" + } + ] + } +} From 0e6e3d10d82871bee0fe7d54998f44d7651f66d0 Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Mon, 7 Dec 2015 17:30:34 +0100 Subject: [PATCH 10/65] Fix LoginSettingsTest (cherry picked from commit bf70927) --- .../console/page/realm/LoginSettings.java | 4 +++ .../org/keycloak/testsuite/page/Form.java | 4 +-- .../console/realm/LoginSettingsTest.java | 32 +++++++++++++++---- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/LoginSettings.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/LoginSettings.java index 2cfcace06e..871167b734 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/LoginSettings.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/LoginSettings.java @@ -80,6 +80,10 @@ public class LoginSettings extends RealmSettings { public void setEmailAsUsername(boolean emailAsUsername) { emailAsUsernameOnOffSwitch.setOn(emailAsUsername); } + + public boolean isEmailAsUsername() { + return emailAsUsernameOnOffSwitch.isOn(); + } public boolean isEditUsernameAllowed() { return editUsernameAllowed.isOn(); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/Form.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/Form.java index 7f664c7134..0d055da1b2 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/Form.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/Form.java @@ -21,9 +21,9 @@ public class Form { public static final String ACTIVE_DIV_XPATH = ".//div[not(contains(@class,'ng-hide'))]"; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Save']") + @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[@kc-save or @data-kc-save]") private WebElement save; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Cancel']") + @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[@kc-reset or @data-kc-reset]") private WebElement cancel; public void save() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java index b087366286..c7a4fec580 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java @@ -41,13 +41,14 @@ import org.keycloak.testsuite.console.page.realm.LoginSettings.RequireSSLOption; import org.keycloak.testsuite.util.MailServer; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; + +import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.Cookie; /** * * @author tkyjovsk */ -@Ignore public class LoginSettingsTest extends AbstractRealmTest { private static final String NEW_USERNAME = "newUsername"; @@ -84,7 +85,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("enabling registration"); loginSettingsPage.form().setRegistrationAllowed(true); + assertTrue(loginSettingsPage.form().isRegistrationAllowed()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("enabled"); testRealmAdminConsolePage.navigateTo(); @@ -98,7 +101,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("enabling email as username"); loginSettingsPage.navigateTo(); loginSettingsPage.form().setEmailAsUsername(true); + assertTrue(loginSettingsPage.form().isEmailAsUsername()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("enabled"); testRealmAdminConsolePage.navigateTo(); @@ -112,8 +117,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("disabling registration"); loginSettingsPage.navigateTo(); loginSettingsPage.form().setRegistrationAllowed(false); - loginSettingsPage.form().save(); assertFalse(loginSettingsPage.form().isRegistrationAllowed()); + loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("disabled"); testRealmAdminConsolePage.navigateTo(); @@ -125,7 +131,9 @@ public class LoginSettingsTest extends AbstractRealmTest { public void editUsername() { log.info("enabling edit username"); loginSettingsPage.form().setEditUsernameAllowed(true); + assertTrue(loginSettingsPage.form().isEditUsernameAllowed()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("enabled"); log.info("edit username"); @@ -145,7 +153,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("disabling edit username"); loginSettingsPage.navigateTo(); loginSettingsPage.form().setEditUsernameAllowed(false); + assertFalse(loginSettingsPage.form().isEditUsernameAllowed()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("disabled"); @@ -156,7 +166,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("enabling reset password"); loginSettingsPage.form().setResetPasswordAllowed(true); + assertTrue(loginSettingsPage.form().isResetPasswordAllowed()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("enabled"); testRealmAdminConsolePage.navigateTo(); @@ -169,8 +181,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("disabling reset password"); loginSettingsPage.navigateTo(); loginSettingsPage.form().setResetPasswordAllowed(false); - loginSettingsPage.form().save(); assertFalse(loginSettingsPage.form().isResetPasswordAllowed()); + loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("disabled"); testRealmAdminConsolePage.navigateTo(); @@ -183,7 +196,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("enabling remember me"); loginSettingsPage.form().setRememberMeAllowed(true); + assertTrue(loginSettingsPage.form().isRememberMeAllowed()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("enabled"); log.info("login with remember me checked"); @@ -198,8 +213,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("disabling remember me"); loginSettingsPage.navigateTo(); loginSettingsPage.form().setRememberMeAllowed(false); - loginSettingsPage.form().save(); assertFalse(loginSettingsPage.form().isRememberMeAllowed()); + loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("disabled"); testAccountPage.navigateTo(); @@ -218,10 +234,12 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("enabling verify email in login settings"); loginSettingsPage.form().setVerifyEmailAllowed(true); + assertTrue(loginSettingsPage.form().isVerifyEmailAllowed()); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("enabled"); - log.info("configure smpt server in test realm"); + log.info("configure smtp server in test realm"); RealmRepresentation testRealmRep = testRealmResource().toRepresentation(); testRealmRep.setSmtpServer(suiteContext.getSmtpServer()); testRealmResource().update(testRealmRep); @@ -236,8 +254,9 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("disabling verify email"); loginSettingsPage.navigateTo(); loginSettingsPage.form().setVerifyEmailAllowed(false); - loginSettingsPage.form().save(); assertFalse(loginSettingsPage.form().isVerifyEmailAllowed()); + loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("disabled"); log.debug("create new test user"); @@ -261,6 +280,7 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("set require ssl for all requests"); loginSettingsPage.form().selectRequireSSL(RequireSSLOption.all); loginSettingsPage.form().save(); + assertFlashMessageSuccess(); log.debug("set"); log.info("check HTTPS required"); From 817127a75130d9cde63e75f2ca734c5277693933 Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Tue, 8 Dec 2015 13:56:17 +0100 Subject: [PATCH 11/65] Fix + refactor SecurityDefensesTest (cherry picked from commit a652173) --- .../page/realm/BruteForceDetection.java | 8 +- .../console/realm/SecurityDefensesTest.java | 193 +++++++----------- 2 files changed, 77 insertions(+), 124 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java index 75e54b2087..e772a5d680 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java @@ -73,7 +73,7 @@ public class BruteForceDetection extends SecurityDefenses { } public void setWaitIncrementSelect(TimeSelectValues value) { - waitIncrementSelect.selectByVisibleText(value.getName()); + waitIncrementSelect.selectByValue(value.getName()); } public void setQuickLoginCheckInput(String value) { @@ -85,7 +85,7 @@ public class BruteForceDetection extends SecurityDefenses { } public void setMinQuickLoginWaitSelect(TimeSelectValues value) { - minQuickLoginWaitSelect.selectByVisibleText(value.getName()); + minQuickLoginWaitSelect.selectByValue(value.getName()); } public void setMaxWaitInput(String value) { @@ -93,7 +93,7 @@ public class BruteForceDetection extends SecurityDefenses { } public void setMaxWaitSelect(TimeSelectValues value) { - maxWaitSelect.selectByVisibleText(value.getName()); + maxWaitSelect.selectByValue(value.getName()); } public void setFailureResetTimeInput(String value) { @@ -101,7 +101,7 @@ public class BruteForceDetection extends SecurityDefenses { } public void setFailureResetTimeSelect(TimeSelectValues value) { - failureResetTimeSelect.selectByVisibleText(value.getName()); + failureResetTimeSelect.selectByValue(value.getName()); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java index e5b8e74038..fcbe8ffdc9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java @@ -19,28 +19,31 @@ package org.keycloak.testsuite.console.realm; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.keycloak.testsuite.auth.page.account.Account; import org.keycloak.testsuite.console.page.realm.BruteForceDetection; import org.keycloak.testsuite.console.page.users.UserAttributes; import org.keycloak.testsuite.console.page.users.Users; -import org.openqa.selenium.By; - -import java.util.Date; - -import static org.jboss.arquillian.graphene.Graphene.waitGui; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; import static org.keycloak.testsuite.admin.Users.setPasswordFor; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; +import static org.keycloak.testsuite.util.WaitUtils.*; +import static org.junit.Assert.*; /** * @author Filip Kiss * @author mhajas + * @author Vaclav Muzikar */ -@Ignore public class SecurityDefensesTest extends AbstractRealmTest { + + public static final String INVALID_PWD_MSG = "Invalid username or password."; + public static final String ACC_DISABLED_MSG = "Account is temporarily disabled, contact admin or try again later."; + public static final short ATTEMPTS_BAD_PWD = 2; + public static final short ATTEMPTS_GOOD_PWD = 1; @Page private BruteForceDetection bruteForceDetectionPage; @@ -54,6 +57,9 @@ public class SecurityDefensesTest extends AbstractRealmTest { @Page private UserAttributes userAttributesPage; + @FindBy(className = "kc-feedback-text") + private WebElement feedbackTextElement; + @Override public void setDefaultPageUriParameters() { super.setDefaultPageUriParameters(); @@ -66,157 +72,72 @@ public class SecurityDefensesTest extends AbstractRealmTest { } @Test - public void maxLoginFailuresTest() { - int secondsToWait = 3; + public void maxLoginFailuresTest() throws InterruptedException { + final short secondsToWait = 3; + final short maxLoginFailures = 2; bruteForceDetectionPage.form().setProtectionEnabled(true); - bruteForceDetectionPage.form().setMaxLoginFailures("1"); + bruteForceDetectionPage.form().setMaxLoginFailures(String.valueOf(maxLoginFailures)); bruteForceDetectionPage.form().setWaitIncrementSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setWaitIncrementInput(String.valueOf(secondsToWait)); + bruteForceDetectionPage.form().setQuickLoginCheckInput("1"); bruteForceDetectionPage.form().save(); assertAlertSuccess(); - testRealmAccountPage.navigateTo(); - - setPasswordFor(testUser, PASSWORD + "-mismatch"); - - testRealmLoginPage.form().login(testUser); - waitForFeedbackText("Invalid username or password."); - Date endTime = new Date(new Date().getTime() + secondsToWait * 1000); - - testRealmLoginPage.form().login(testUser); - waitGui().until().element(By.className("instruction")) - .text().contains("Account is temporarily disabled, contact admin or try again later."); - endTime = new Date(endTime.getTime() + secondsToWait * 1000); - testRealmAccountPage.navigateTo(); - testRealmLoginPage.form().login(testUser); - endTime = new Date(endTime.getTime() + secondsToWait * 1000); - - while (new Date().compareTo(endTime) < 0) { - try { - Thread.sleep(50); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - setPasswordFor(testUser, PASSWORD); - testRealmLoginPage.form().login(testUser); - assertCurrentUrlStartsWith(testRealmAccountPage); + tryToLogin(secondsToWait * (ATTEMPTS_BAD_PWD + ATTEMPTS_GOOD_PWD) / maxLoginFailures); } @Test - public void quickLoginCheck() { - int secondsToWait = 3; + public void quickLoginCheck() throws InterruptedException { + final short secondsToWait = 3; bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("100"); - bruteForceDetectionPage.form().setQuickLoginCheckInput("1500"); + bruteForceDetectionPage.form().setQuickLoginCheckInput("10000"); bruteForceDetectionPage.form().setMinQuickLoginWaitSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setMinQuickLoginWaitInput(String.valueOf(secondsToWait)); bruteForceDetectionPage.form().save(); assertAlertSuccess(); - testRealmAccountPage.navigateTo(); - - setPasswordFor(testUser, PASSWORD + "-mismatch"); - - testRealmLoginPage.form().login(testUser); - testRealmLoginPage.form().login(testUser); - Date endTime = new Date(new Date().getTime() + secondsToWait * 1000); - testRealmLoginPage.form().login(testUser); - waitGui().until().element(By.className("instruction")) - .text().contains("Account is temporarily disabled, contact admin or try again later."); - endTime = new Date(endTime.getTime() + secondsToWait * 1000); - - testRealmAccountPage.navigateTo(); - testRealmLoginPage.form().login(testUser); - endTime = new Date(endTime.getTime() + secondsToWait * 1000); - - while (new Date().compareTo(endTime) < 0) { - try { - Thread.sleep(50); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - setPasswordFor(testUser, PASSWORD); - testRealmLoginPage.form().login(testUser); - assertCurrentUrlStartsWith(testRealmAccountPage); + tryToLogin(secondsToWait); } @Test - public void maxWaitLoginFailures() { - int secondsToWait = 5; + public void maxWaitLoginFailures() throws InterruptedException { + final short secondsToWait = 5; bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("1"); + bruteForceDetectionPage.form().setWaitIncrementSelect(BruteForceDetection.TimeSelectValues.SECONDS); + bruteForceDetectionPage.form().setWaitIncrementInput("10"); bruteForceDetectionPage.form().setMaxWaitSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setMaxWaitInput(String.valueOf(secondsToWait)); bruteForceDetectionPage.form().save(); - testRealmAccountPage.navigateTo(); - - setPasswordFor(testUser, PASSWORD + "-mismatch"); - - testRealmLoginPage.form().login(testUser); - Date endTime = new Date(new Date().getTime() + secondsToWait * 1000); - waitForFeedbackText("Invalid username or password."); - - testRealmLoginPage.form().login(testUser); - endTime = new Date(endTime.getTime() + secondsToWait * 1000); - waitGui().until().element(By.className("instruction")) - .text().contains("Account is temporarily disabled, contact admin or try again later."); - testRealmAccountPage.navigateTo(); - testRealmLoginPage.form().login(testUser); - endTime = new Date(endTime.getTime() + secondsToWait * 1000); - waitForFeedbackText("Account is temporarily disabled, contact admin or try again later."); - - while (new Date().compareTo(endTime) < 0) { - try { - Thread.sleep(50); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - setPasswordFor(testUser, PASSWORD); - testRealmLoginPage.form().login(testUser); - assertCurrentUrlStartsWith(testRealmAccountPage); + tryToLogin(secondsToWait); } @Test - public void failureResetTime() { - int secondsToWait = 3; + public void failureResetTime() throws InterruptedException { + final short failureResetTime = 3; + final short waitIncrement = 3; bruteForceDetectionPage.form().setProtectionEnabled(true); - bruteForceDetectionPage.form().setMaxLoginFailures("2"); + bruteForceDetectionPage.form().setMaxLoginFailures("1"); + bruteForceDetectionPage.form().setWaitIncrementSelect(BruteForceDetection.TimeSelectValues.SECONDS); + bruteForceDetectionPage.form().setWaitIncrementInput(String.valueOf(waitIncrement)); bruteForceDetectionPage.form().setFailureResetTimeSelect(BruteForceDetection.TimeSelectValues.SECONDS); - bruteForceDetectionPage.form().setFailureResetTimeInput(String.valueOf(secondsToWait)); + bruteForceDetectionPage.form().setFailureResetTimeInput(String.valueOf(failureResetTime)); bruteForceDetectionPage.form().save(); assertAlertSuccess(); - testRealmAccountPage.navigateTo(); - - setPasswordFor(testUser, PASSWORD + "-mismatch"); + tryToLogin(failureResetTime, false); testRealmLoginPage.form().login(testUser); - waitForFeedbackText("Invalid username or password."); - Date endTime = new Date(new Date().getTime() + secondsToWait * 1000); + assertFeedbackText(ACC_DISABLED_MSG); - while (new Date().compareTo(endTime) < 0) { - try { - Thread.sleep(50); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + Thread.sleep(waitIncrement * 1000); - testRealmLoginPage.form().login(testUser); - waitForFeedbackText("Invalid username or password."); - - setPasswordFor(testUser, PASSWORD); testRealmLoginPage.form().login(testUser); assertCurrentUrlStartsWith(testRealmAccountPage); } @@ -240,6 +161,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { usersPage.table().searchUsers(testUser.getUsername()); usersPage.table().editUser(testUser.getUsername()); + assertFalse(userAttributesPage.form().isEnabled()); userAttributesPage.form().unlockUser(); testRealmAccountPage.navigateTo(); @@ -250,8 +172,39 @@ public class SecurityDefensesTest extends AbstractRealmTest { assertCurrentUrlStartsWith(testRealmAccountPage); } - private void waitForFeedbackText(String text) { - waitGui().until().element(By.className("kc-feedback-text")) - .text().contains(text); + private void assertFeedbackText(String text) { + waitGuiForElement(feedbackTextElement); + assertEquals(text, feedbackTextElement.getText()); + } + + private void tryToLogin(int wait) throws InterruptedException { + tryToLogin(wait, true); + } + + private void tryToLogin(int wait, boolean finalLogin) throws InterruptedException { + testRealmAccountPage.navigateTo(); + + setPasswordFor(testUser, PASSWORD + "-mismatch"); + + for (int i = 0; i < ATTEMPTS_BAD_PWD; i++) { + testRealmLoginPage.form().login(testUser); + assertFeedbackText(INVALID_PWD_MSG); + } + + setPasswordFor(testUser, PASSWORD); + for (int i = 0; i < ATTEMPTS_GOOD_PWD; i++) { + testRealmLoginPage.form().login(testUser); + assertFeedbackText(ACC_DISABLED_MSG); + } + + wait *= 1000; + + log.debug("Wait: " + wait); + Thread.sleep(wait); + + if (finalLogin) { + testRealmLoginPage.form().login(testUser); + assertCurrentUrlStartsWith(testRealmAccountPage); + } } } From c51d5e98202d692d4a555167d820ec7750d918cd Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Thu, 10 Dec 2015 12:02:28 +0100 Subject: [PATCH 12/65] Fix PasswordPolicyTest (cherry picked from commit 87b125e) --- .../testsuite/console/page/users/UserCredentials.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java index 830d789c9d..888f208bec 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java @@ -4,6 +4,7 @@ import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; import static org.keycloak.testsuite.page.Form.setInputValue; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; +import static org.keycloak.testsuite.util.WaitUtils.*; /** * @@ -25,7 +26,7 @@ public class UserCredentials extends User { @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='temporaryPassword']]") private OnOffSwitch temporaryOnOffSwitch; - @FindBy(xpath = ".//button[contains(@data-ng-click, 'resetPassword')]") + @FindBy(xpath = ".//div[not(contains(@class, 'ng-hide'))]/button[contains(@data-ng-click, 'resetPassword')]") private WebElement resetPasswordButton; public void setNewPassword(String newPassword) { @@ -41,6 +42,7 @@ public class UserCredentials extends User { } public void clickResetPasswordAndConfirm() { + waitGuiForElement(resetPasswordButton); resetPasswordButton.click(); modalDialog.ok(); } From 53a542d1e29e94c8e8279edfbd891139fb46b35a Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Thu, 10 Dec 2015 12:16:35 +0100 Subject: [PATCH 13/65] Enable TokensTest (already fixed by previous commit bf70927) (cherry picked from commit 6783bcb) --- .../java/org/keycloak/testsuite/console/realm/TokensTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java index 33cfeb56b4..975a6199a6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java @@ -31,7 +31,6 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; * * @author Petr Mensik */ -@Ignore public class TokensTest extends AbstractRealmTest { @Page From eaf326dd373d57552c9b15eba488431a715c62d9 Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Fri, 18 Dec 2015 15:49:43 +0100 Subject: [PATCH 14/65] Fix Client Mappers tests (cherry picked from commit b9f9db0) --- .../mappers/CreateClientMappersForm.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/mappers/CreateClientMappersForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/mappers/CreateClientMappersForm.java index fb4dacf4c5..73957bc289 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/mappers/CreateClientMappersForm.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/mappers/CreateClientMappersForm.java @@ -43,40 +43,40 @@ public class CreateClientMappersForm extends Form { @FindBy(id = "mapperTypeCreate") private Select mapperTypeSelect; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Property']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Property']//following-sibling::node()//input[@type='text']") private WebElement propertyInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='User Attribute']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='User Attribute']//following-sibling::node()//input[@type='text']") private WebElement userAttributeInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='User Session Note']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='User Session Note']//following-sibling::node()//input[@type='text']") private WebElement userSessionNoteInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Multivalued']//following-sibling::node()//div[@class='onoffswitch']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Multivalued']//following-sibling::node()//div[@class='onoffswitch']") private OnOffSwitch multivaluedInput; @FindBy(xpath = ".//button[text() = 'Select Role']/../..//input") private WebElement roleInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='New Role Name']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='New Role Name']//following-sibling::node()//input[@type='text']") private WebElement newRoleInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Token Claim Name']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Token Claim Name']//following-sibling::node()//input[@type='text']") private WebElement tokenClaimNameInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Claim value']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Claim value']//following-sibling::node()//input[@type='text']") private WebElement tokenClaimValueInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Claim JSON Type']//following-sibling::node()//select") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Claim JSON Type']//following-sibling::node()//select") private Select claimJSONTypeInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Add to ID token']//following-sibling::node()//div[@class='onoffswitch']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Add to ID token']//following-sibling::node()//div[@class='onoffswitch']") private OnOffSwitch addToIDTokenInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Add to access token']//following-sibling::node()//div[@class='onoffswitch']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Add to access token']//following-sibling::node()//div[@class='onoffswitch']") private OnOffSwitch addToAccessTokenInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Full group path']//following-sibling::node()//div[@class='onoffswitch']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Full group path']//following-sibling::node()//div[@class='onoffswitch']") private OnOffSwitch fullGroupPath; @FindBy(xpath = ".//button[text() = 'Select Role']") @@ -260,25 +260,25 @@ public class CreateClientMappersForm extends Form { } //SAML - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Role attribute name']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Role attribute name']//following-sibling::node()//input[@type='text']") private WebElement roleAttributeNameInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Friendly Name']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Friendly Name']//following-sibling::node()//input[@type='text']") private WebElement friendlyNameInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='SAML Attribute NameFormat']//following-sibling::node()//select") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='SAML Attribute NameFormat']//following-sibling::node()//select") private Select samlAttributeNameFormatSelect; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Single Role Attribute']//following-sibling::node()//div[@class='onoffswitch']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Single Role Attribute']//following-sibling::node()//div[@class='onoffswitch']") private OnOffSwitch singleRoleAttributeSwitch; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Attribute value']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Attribute value']//following-sibling::node()//input[@type='text']") private WebElement attributeValueInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Group attribute name']//following-sibling::node()//input[@type='text']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Group attribute name']//following-sibling::node()//input[@type='text']") private WebElement groupAttributeNameInput; - @FindBy(xpath = ".//div[@properties='mapperType.properties']//label[text()='Single Group Attribute']//following-sibling::node()//div[@class='onoffswitch']") + @FindBy(xpath = ".//div[@properties='model.mapperType.properties']//label[text()='Single Group Attribute']//following-sibling::node()//div[@class='onoffswitch']") private OnOffSwitch singleGroupAttributeSwitch; public void setRoleAttributeName(String value) { From 42d17577d0e103e508a8b8a8f0fbc14c5b03838a Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Fri, 18 Dec 2015 17:28:57 +0100 Subject: [PATCH 15/65] Upstream sync --- .../console/page/users/UserCredentials.java | 2 +- .../console/realm/LoginSettingsTest.java | 26 +++++++++---------- .../console/realm/SecurityDefensesTest.java | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java index 888f208bec..0652fa4506 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserCredentials.java @@ -42,7 +42,7 @@ public class UserCredentials extends User { } public void clickResetPasswordAndConfirm() { - waitGuiForElement(resetPasswordButton); + waitUntilElement(resetPasswordButton); resetPasswordButton.click(); modalDialog.ok(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java index c7a4fec580..0a6b6a5632 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/LoginSettingsTest.java @@ -24,7 +24,6 @@ import org.junit.Assert; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; import org.keycloak.representations.idm.RealmRepresentation; @@ -42,7 +41,6 @@ import org.keycloak.testsuite.util.MailServer; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; -import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.Cookie; /** @@ -87,7 +85,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setRegistrationAllowed(true); assertTrue(loginSettingsPage.form().isRegistrationAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("enabled"); testRealmAdminConsolePage.navigateTo(); @@ -103,7 +101,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setEmailAsUsername(true); assertTrue(loginSettingsPage.form().isEmailAsUsername()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("enabled"); testRealmAdminConsolePage.navigateTo(); @@ -119,7 +117,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setRegistrationAllowed(false); assertFalse(loginSettingsPage.form().isRegistrationAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("disabled"); testRealmAdminConsolePage.navigateTo(); @@ -133,7 +131,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setEditUsernameAllowed(true); assertTrue(loginSettingsPage.form().isEditUsernameAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("enabled"); log.info("edit username"); @@ -155,7 +153,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setEditUsernameAllowed(false); assertFalse(loginSettingsPage.form().isEditUsernameAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("disabled"); @@ -168,7 +166,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setResetPasswordAllowed(true); assertTrue(loginSettingsPage.form().isResetPasswordAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("enabled"); testRealmAdminConsolePage.navigateTo(); @@ -183,7 +181,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setResetPasswordAllowed(false); assertFalse(loginSettingsPage.form().isResetPasswordAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("disabled"); testRealmAdminConsolePage.navigateTo(); @@ -198,7 +196,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setRememberMeAllowed(true); assertTrue(loginSettingsPage.form().isRememberMeAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("enabled"); log.info("login with remember me checked"); @@ -215,7 +213,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setRememberMeAllowed(false); assertFalse(loginSettingsPage.form().isRememberMeAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("disabled"); testAccountPage.navigateTo(); @@ -236,7 +234,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setVerifyEmailAllowed(true); assertTrue(loginSettingsPage.form().isVerifyEmailAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("enabled"); log.info("configure smtp server in test realm"); @@ -256,7 +254,7 @@ public class LoginSettingsTest extends AbstractRealmTest { loginSettingsPage.form().setVerifyEmailAllowed(false); assertFalse(loginSettingsPage.form().isVerifyEmailAllowed()); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("disabled"); log.debug("create new test user"); @@ -280,7 +278,7 @@ public class LoginSettingsTest extends AbstractRealmTest { log.info("set require ssl for all requests"); loginSettingsPage.form().selectRequireSSL(RequireSSLOption.all); loginSettingsPage.form().save(); - assertFlashMessageSuccess(); + assertAlertSuccess(); log.debug("set"); log.info("check HTTPS required"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java index fcbe8ffdc9..72bcd7f6d4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java @@ -173,7 +173,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { } private void assertFeedbackText(String text) { - waitGuiForElement(feedbackTextElement); + waitUntilElement(feedbackTextElement); assertEquals(text, feedbackTextElement.getText()); } From d939b6a431dc0ccd0fbbc610b4525f86fb7f323e Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 18 Dec 2015 17:15:27 -0500 Subject: [PATCH 16/65] template scope --- .../META-INF/jpa-changelog-1.8.0.xml | 25 ++- .../main/resources/META-INF/persistence.xml | 1 + .../idm/ClientRepresentation.java | 29 ++++ .../idm/ClientTemplateRepresentation.java | 9 + .../theme/base/admin/resources/js/app.js | 18 ++ .../admin/resources/js/controllers/clients.js | 128 +++++++++++++- .../theme/base/admin/resources/js/services.js | 46 +++++ .../resources/partials/client-mappers.html | 15 +- .../partials/client-scope-mappings.html | 26 ++- .../client-template-scope-mappings.html | 117 +++++++++++++ .../templates/kc-tabs-client-template.html | 4 + .../resource/ClientTemplateResource.java | 5 + .../java/org/keycloak/models/ClientModel.java | 18 +- .../keycloak/models/ClientTemplateModel.java | 2 +- .../keycloak/models/ScopeContainerModel.java | 24 +++ .../org/keycloak/models/ScopeMapperModel.java | 9 - .../models/entities/ClientEntity.java | 27 +++ .../models/entities/ClientTemplateEntity.java | 18 ++ .../models/utils/KeycloakModelUtils.java | 19 +++ .../models/utils/ModelToRepresentation.java | 4 + .../models/utils/RepresentationToModel.java | 39 ++++- .../cache/infinispan/ClientAdapter.java | 60 ++++--- .../infinispan/ClientTemplateAdapter.java | 64 +++++++ .../models/cache/entities/CachedClient.java | 18 ++ .../cache/entities/CachedClientTemplate.java | 14 ++ .../keycloak/models/jpa/ClientAdapter.java | 61 ++++--- .../models/jpa/ClientTemplateAdapter.java | 85 ++++++++++ .../keycloak/models/jpa/JpaRealmProvider.java | 3 + .../org/keycloak/models/jpa/RealmAdapter.java | 4 + .../models/jpa/entities/ClientEntity.java | 33 ++++ .../jpa/entities/ClientTemplateEntity.java | 10 ++ .../entities/TemplateScopeMappingEntity.java | 99 +++++++++++ .../keycloak/adapters/ClientAdapter.java | 51 ++++-- .../adapters/ClientTemplateAdapter.java | 63 +++++++ .../models/mongo/utils/MongoModelUtils.java | 16 ++ .../keycloak/protocol/oidc/TokenManager.java | 19 ++- .../admin/ClientTemplateResource.java | 24 ++- .../admin/ScopeMappedClientResource.java | 22 +-- .../resources/admin/ScopeMappedResource.java | 32 ++-- .../testsuite/account/AccountTest.java | 2 +- .../keycloak/testsuite/model/ImportTest.java | 3 +- .../testsuite/oauth/AccessTokenTest.java | 160 +++++++++++++++++- 42 files changed, 1294 insertions(+), 132 deletions(-) create mode 100755 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-scope-mappings.html create mode 100755 model/api/src/main/java/org/keycloak/models/ScopeContainerModel.java delete mode 100755 model/api/src/main/java/org/keycloak/models/ScopeMapperModel.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/TemplateScopeMappingEntity.java diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml index db2a791fcc..068d4e7187 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml @@ -15,6 +15,17 @@ + + + + + + + + + + + @@ -24,12 +35,21 @@ + + + + + + + + + - + @@ -46,6 +66,9 @@ + + + diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index 36f33bfe76..b2af448261 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -36,6 +36,7 @@ org.keycloak.models.jpa.entities.GroupRoleMappingEntity org.keycloak.models.jpa.entities.UserGroupMembershipEntity org.keycloak.models.jpa.entities.ClientTemplateEntity + org.keycloak.models.jpa.entities.TemplateScopeMappingEntity org.keycloak.events.jpa.EventEntity diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index 2384a2ed3b..ab81db5405 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -41,6 +41,10 @@ public class ClientRepresentation { protected Map registeredNodes; protected List protocolMappers; protected String clientTemplate; + private Boolean useTemplateConfig; + private Boolean useTemplateScope; + private Boolean useTemplateMappers; + public String getId() { return id; @@ -298,4 +302,29 @@ public class ClientRepresentation { public void setClientTemplate(String clientTemplate) { this.clientTemplate = clientTemplate; } + + public Boolean isUseTemplateConfig() { + return useTemplateConfig; + } + + public void setUseTemplateConfig(Boolean useTemplateConfig) { + this.useTemplateConfig = useTemplateConfig; + } + + public Boolean isUseTemplateScope() { + return useTemplateScope; + } + + public void setUseTemplateScope(Boolean useTemplateScope) { + this.useTemplateScope = useTemplateScope; + } + + public Boolean isUseTemplateMappers() { + return useTemplateMappers; + } + + public void setUseTemplateMappers(Boolean useTemplateMappers) { + this.useTemplateMappers = useTemplateMappers; + } + } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java index f0bf09e392..dc575c4109 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java @@ -16,6 +16,7 @@ public class ClientTemplateRepresentation { protected String name; protected String description; protected String protocol; + protected Boolean fullScopeAllowed; protected List protocolMappers; public String getId() { @@ -58,4 +59,12 @@ public class ClientTemplateRepresentation { public void setProtocol(String protocol) { this.protocol = protocol; } + + public Boolean isFullScopeAllowed() { + return fullScopeAllowed; + } + + public void setFullScopeAllowed(Boolean fullScopeAllowed) { + this.fullScopeAllowed = fullScopeAllowed; + } } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index f7a3c25708..cebb3d4cf9 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1088,6 +1088,9 @@ module.config([ '$routeProvider', function($routeProvider) { client : function(ClientLoader) { return ClientLoader(); }, + templates : function(ClientTemplateListLoader) { + return ClientTemplateListLoader(); + }, clients : function(ClientListLoader) { return ClientListLoader(); } @@ -1202,6 +1205,21 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientTemplateDetailCtrl' }) + .when('/realms/:realm/client-templates/:template/scope-mappings', { + templateUrl : resourceUrl + '/partials/client-template-scope-mappings.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + template : function(ClientTemplateLoader) { + return ClientTemplateLoader(); + }, + clients : function(ClientListLoader) { + return ClientListLoader(); + } + }, + controller : 'ClientTemplateScopeMappingCtrl' + }) .when('/realms/:realm/clients', { templateUrl : resourceUrl + '/partials/client-list.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index a6a9130f4a..ec28ef45b0 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1089,8 +1089,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, }; }); -module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, Notifications, - Client, +module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, templates, Notifications, + Client, ClientTemplate, ClientRealmScopeMapping, ClientClientScopeMapping, ClientRole, ClientAvailableRealmScopeMapping, ClientAvailableClientScopeMapping, ClientCompositeRealmScopeMapping, ClientCompositeClientScopeMapping) { @@ -1107,8 +1107,20 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien $scope.clientMappings = []; $scope.dummymodel = []; + if (client.clientTemplate) { + for (var i = 0; i < templates.length; i++) { + if (templates[i].name == client.clientTemplate) { + ClientTemplate.get({realm: realm.realm, template: templates[i].id}, function(data) { + $scope.template = data; + }); + break; + } + } - $scope.changeFullScopeAllowed = function() { + } + + + $scope.changeFlag = function() { Client.update({ realm : realm.realm, client : client.id @@ -1122,6 +1134,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien + function updateRealmRoles() { $scope.realmRoles = ClientAvailableRealmScopeMapping.query({realm : realm.realm, client : client.id}); $scope.realmMappings = ClientRealmScopeMapping.query({realm : realm.realm, client : client.id}); @@ -1420,6 +1433,7 @@ module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client }); module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client, templates, serverInfo, + Client, ClientProtocolMappersByProtocol, ClientProtocolMapper, $route, Dialog, Notifications) { $scope.realm = realm; @@ -1435,6 +1449,16 @@ module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client } } } + $scope.changeFlag = function() { + Client.update({ + realm : realm.realm, + client : client.id + }, $scope.client, function() { + $scope.changed = false; + client = angular.copy($scope.client); + Notifications.success("Client updated."); + }); + } var protocolMappers = serverInfo.protocolMapperTypes[client.protocol]; var mapperTypes = {}; @@ -1910,6 +1934,104 @@ module.controller('ClientTemplateAddBuiltinProtocolMapperCtrl', function($scope, }); +module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, realm, template, clients, Notifications, + ClientTemplate, + ClientTemplateRealmScopeMapping, ClientTemplateClientScopeMapping, ClientRole, + ClientTemplateAvailableRealmScopeMapping, ClientTemplateAvailableClientScopeMapping, + ClientTemplateCompositeRealmScopeMapping, ClientTemplateCompositeClientScopeMapping) { + $scope.realm = realm; + $scope.template = angular.copy(template); + $scope.selectedRealmRoles = []; + $scope.selectedRealmMappings = []; + $scope.realmMappings = []; + $scope.clients = clients; + $scope.clientRoles = []; + $scope.clientComposite = []; + $scope.selectedClientRoles = []; + $scope.selectedClientMappings = []; + $scope.clientMappings = []; + $scope.dummymodel = []; + + + $scope.changeFullScopeAllowed = function() { + ClientTemplate.update({ + realm : realm.realm, + template : template.id + }, $scope.template, function() { + $scope.changed = false; + template = angular.copy($scope.template); + updateTemplateRealmRoles(); + Notifications.success("Scope mappings updated."); + }); + } + + + + function updateTemplateRealmRoles() { + $scope.realmRoles = ClientTemplateAvailableRealmScopeMapping.query({realm : realm.realm, template : template.id}); + $scope.realmMappings = ClientTemplateRealmScopeMapping.query({realm : realm.realm, template : template.id}); + $scope.realmComposite = ClientTemplateCompositeRealmScopeMapping.query({realm : realm.realm, template : template.id}); + } + + function updateTemplateClientRoles() { + if ($scope.targetClient) { + $scope.clientRoles = ClientTemplateAvailableClientScopeMapping.query({realm : realm.realm, template : template.id, targetClient : $scope.targetClient.id}); + $scope.clientMappings = ClientTemplateClientScopeMapping.query({realm : realm.realm, template : template.id, targetClient : $scope.targetClient.id}); + $scope.clientComposite = ClientTemplateCompositeClientScopeMapping.query({realm : realm.realm, template : template.id, targetClient : $scope.targetClient.id}); + } else { + $scope.clientRoles = null; + $scope.clientMappings = null; + $scope.clientComposite = null; + } + } + + $scope.changeClient = function() { + updateTemplateClientRoles(); + }; + + $scope.addRealmRole = function() { + var roles = $scope.selectedRealmRoles; + $scope.selectedRealmRoles = []; + $http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm', + roles).success(function() { + updateTemplateRealmRoles(); + Notifications.success("Scope mappings updated."); + }); + }; + + $scope.deleteRealmRole = function() { + 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 () { + updateTemplateRealmRoles(); + Notifications.success("Scope mappings updated."); + }); + }; + + $scope.addClientRole = function() { + 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 () { + updateTemplateClientRoles(); + Notifications.success("Scope mappings updated."); + }); + }; + + $scope.deleteClientRole = function() { + 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 () { + updateTemplateClientRoles(); + Notifications.success("Scope mappings updated."); + }); + }; + + updateTemplateRealmRoles(); +}); + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 7a47a83a0c..b0d95673e7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -848,6 +848,52 @@ module.factory('ClientTemplateProtocolMappersByProtocol', function($resource) { }); }); +module.factory('ClientTemplateRealmScopeMapping', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/realm', { + realm : '@realm', + template : '@template' + }); +}); + +module.factory('ClientTemplateAvailableRealmScopeMapping', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/realm/available', { + realm : '@realm', + template : '@template' + }); +}); + +module.factory('ClientTemplateCompositeRealmScopeMapping', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/realm/composite', { + realm : '@realm', + template : '@template' + }); +}); + +module.factory('ClientTemplateClientScopeMapping', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/clients/:targetClient', { + realm : '@realm', + template : '@template', + targetClient : '@targetClient' + }); +}); + +module.factory('ClientTemplateAvailableClientScopeMapping', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/clients/:targetClient/available', { + realm : '@realm', + template : '@template', + targetClient : '@targetClient' + }); +}); + +module.factory('ClientTemplateCompositeClientScopeMapping', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/clients/:targetClient/composite', { + realm : '@realm', + template : '@template', + targetClient : '@targetClient' + }); +}); + + module.factory('ClientSessionStats', function($resource) { return $resource(authUrl + '/admin/realms/:realm/clients/:client/session-stats', { realm : '@realm', diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html index 9b98065eba..86b52e103c 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html @@ -7,6 +7,20 @@ +

+
+
+ + Inherit mappers from client template +
+ +
+ +
+
+
@@ -24,7 +38,6 @@ diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html index cd63f49afe..8075065287 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html @@ -11,17 +11,37 @@

-
+
+ + Inherit scope from client template +
+ +
+ +
+
{{:: 'full-scope-allowed.tooltip' | translate}}
- + +
+
+
+ + Client template has full scope allowed, which means this client will have the full scope of all roles. +
+ +
+
+ inherited
- +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-scope-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-scope-mappings.html new file mode 100755 index 0000000000..690307114e --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-scope-mappings.html @@ -0,0 +1,117 @@ +
+ + + + + +

{{template.name}} {{:: 'scope-mappings' | translate}}

+

+ +
+
+ + {{:: 'full-scope-allowed.tooltip' | translate}} +
+ +
+
+
+ + +
+
+ +
+
+
+ + {{:: 'scope.available-roles.tooltip' | translate}} + + + +
+
+ + {{:: 'assigned-roles.tooltip' | translate}} + + +
+
+ + {{:: 'realm.effective-roles.tooltip' | translate}} + +
+
+
+
+ +
+ + +
+
+
{{:: 'select-client-roles.tooltip' | translate}}
+
+
+
+ + {{:: 'assign.available-roles.tooltip' | translate}} + + +
+
+ + {{:: 'client.assigned-roles.tooltip' | translate}} + + +
+
+ + {{:: 'client.effective-roles.tooltip' | translate}} + +
+
+
+
+ +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html index 728e0917d7..0bbdaf0a22 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html @@ -12,5 +12,9 @@ {{:: 'mappers' | translate}} {{:: 'mappers.tooltip' | translate}} +
  • + {{:: 'scope' | translate}} + {{:: 'scope.tooltip' | translate}} +
  • \ No newline at end of file diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java index 0a5d8ff417..a5665cd901 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java @@ -27,6 +27,9 @@ public interface ClientTemplateResource { @Path("protocol-mappers") public ProtocolMappersResource getProtocolMappers(); + @Path("/scope-mappings") + public RoleMappingResource getScopeMappings(); + @GET @Produces(MediaType.APPLICATION_JSON) public ClientTemplateRepresentation toRepresentation(); @@ -37,4 +40,6 @@ public interface ClientTemplateResource { @DELETE public void remove(); + + } \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index f7e0305a42..475edf205d 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -8,7 +8,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public interface ClientModel extends RoleContainerModel, ProtocolMapperContainerModel { +public interface ClientModel extends RoleContainerModel, ProtocolMapperContainerModel, ScopeContainerModel { // COMMON ATTRIBUTES @@ -74,7 +74,6 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine void updateDefaultRoles(String[] defaultRoles); - Set getClientScopeMappings(ClientModel client); boolean isBearerOnly(); void setBearerOnly(boolean only); @@ -93,9 +92,6 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine String getRegistrationToken(); void setRegistrationToken(String registrationToken); - boolean isFullScopeAllowed(); - void setFullScopeAllowed(boolean value); - String getProtocol(); void setProtocol(String protocol); @@ -126,16 +122,16 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine boolean isServiceAccountsEnabled(); void setServiceAccountsEnabled(boolean serviceAccountsEnabled); - Set getScopeMappings(); - void addScopeMapping(RoleModel role); - void deleteScopeMapping(RoleModel role); - Set getRealmScopeMappings(); - boolean hasScope(RoleModel role); - RealmModel getRealm(); ClientTemplateModel getClientTemplate(); void setClientTemplate(ClientTemplateModel template); + boolean useTemplateScope(); + void setUseTemplateScope(boolean flag); + boolean useTemplateMappers(); + void setUseTemplateMappers(boolean flag); + boolean useTemplateConfig(); + void setUseTemplateConfig(boolean flag); /** * Time in seconds since epoc diff --git a/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java index f7d67ac18e..f3c0f59954 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java @@ -8,7 +8,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public interface ClientTemplateModel extends ProtocolMapperContainerModel { +public interface ClientTemplateModel extends ProtocolMapperContainerModel, ScopeContainerModel { String getId(); String getName(); diff --git a/model/api/src/main/java/org/keycloak/models/ScopeContainerModel.java b/model/api/src/main/java/org/keycloak/models/ScopeContainerModel.java new file mode 100755 index 0000000000..9bc99ada5c --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ScopeContainerModel.java @@ -0,0 +1,24 @@ +package org.keycloak.models; + +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ScopeContainerModel { + boolean isFullScopeAllowed(); + + void setFullScopeAllowed(boolean value); + + Set getScopeMappings(); + + void addScopeMapping(RoleModel role); + + void deleteScopeMapping(RoleModel role); + + Set getRealmScopeMappings(); + + boolean hasScope(RoleModel role); + +} diff --git a/model/api/src/main/java/org/keycloak/models/ScopeMapperModel.java b/model/api/src/main/java/org/keycloak/models/ScopeMapperModel.java deleted file mode 100755 index 4619eb4745..0000000000 --- a/model/api/src/main/java/org/keycloak/models/ScopeMapperModel.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.keycloak.models; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface ScopeMapperModel { - -} diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index 24e5fc655a..04daf9f3aa 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java @@ -49,6 +49,9 @@ public class ClientEntity extends AbstractIdentifiableEntity { private List identityProviders = new ArrayList(); private List protocolMappers = new ArrayList(); private String clientTemplate; + private boolean useTemplateConfig; + private boolean useTemplateScope; + private boolean useTemplateMappers; public String getClientId() { return clientId; @@ -309,5 +312,29 @@ public class ClientEntity extends AbstractIdentifiableEntity { public void setClientTemplate(String clientTemplate) { this.clientTemplate = clientTemplate; } + + public boolean isUseTemplateConfig() { + return useTemplateConfig; + } + + public void setUseTemplateConfig(boolean useTemplateConfig) { + this.useTemplateConfig = useTemplateConfig; + } + + public boolean isUseTemplateScope() { + return useTemplateScope; + } + + public void setUseTemplateScope(boolean useTemplateScope) { + this.useTemplateScope = useTemplateScope; + } + + public boolean isUseTemplateMappers() { + return useTemplateMappers; + } + + public void setUseTemplateMappers(boolean useTemplateMappers) { + this.useTemplateMappers = useTemplateMappers; + } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java index 849eaa12e9..8ca932f0f1 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java @@ -14,6 +14,8 @@ public class ClientTemplateEntity extends AbstractIdentifiableEntity { private String description; private String realmId; private String protocol; + private boolean fullScopeAllowed; + private List scopeIds = new ArrayList(); private List protocolMappers = new ArrayList(); public String getName() { @@ -55,5 +57,21 @@ public class ClientTemplateEntity extends AbstractIdentifiableEntity { public void setProtocol(String protocol) { this.protocol = protocol; } + + public boolean isFullScopeAllowed() { + return fullScopeAllowed; + } + + public void setFullScopeAllowed(boolean fullScopeAllowed) { + this.fullScopeAllowed = fullScopeAllowed; + } + + public List getScopeIds() { + return scopeIds; + } + + public void setScopeIds(List scopeIds) { + this.scopeIds = scopeIds; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 8c6af8862e..f08eee4289 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -15,7 +15,9 @@ import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.ScopeContainerModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderModel; @@ -38,6 +40,7 @@ import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -521,4 +524,20 @@ public final class KeycloakModelUtils { } return found; } + + public static Set getClientScopeMappings(ClientModel client, ScopeContainerModel container) { + Set mappings = container.getScopeMappings(); + Set result = new HashSet<>(); + for (RoleModel role : mappings) { + RoleContainerModel roleContainer = role.getContainer(); + if (roleContainer instanceof ClientModel) { + if (client.getId().equals(((ClientModel)roleContainer).getId())) { + result.add(role); + } + + } + } + return result; + } + } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 7b6a198d76..e891582a63 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -419,6 +419,7 @@ public class ModelToRepresentation { } rep.setProtocolMappers(mappings); } + rep.setFullScopeAllowed(clientModel.isFullScopeAllowed()); return rep; } @@ -476,6 +477,9 @@ public class ModelToRepresentation { } rep.setProtocolMappers(mappings); } + rep.setUseTemplateMappers(clientModel.useTemplateMappers()); + rep.setUseTemplateConfig(clientModel.useTemplateConfig()); + rep.setUseTemplateScope(clientModel.useTemplateScope()); return rep; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 4c7dad1189..2d99c3b2a3 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -802,11 +802,6 @@ public class RepresentationToModel { if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient()); if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol()); - if (resourceRep.isFullScopeAllowed() != null) { - client.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); - } else { - client.setFullScopeAllowed(!client.isConsentRequired()); - } if (resourceRep.getNodeReRegistrationTimeout() != null) { client.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout()); } else { @@ -893,6 +888,26 @@ public class RepresentationToModel { } } + if (resourceRep.isFullScopeAllowed() != null) { + client.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); + } else { + if (client.getClientTemplate() != null) { + client.setFullScopeAllowed(!client.isConsentRequired() && client.getClientTemplate().isFullScopeAllowed()); + + } else { + client.setFullScopeAllowed(!client.isConsentRequired()); + } + } + if (resourceRep.isUseTemplateConfig() != null) client.setUseTemplateConfig(resourceRep.isUseTemplateConfig()); + else client.setUseTemplateConfig(resourceRep.getClientTemplate() != null); + + if (resourceRep.isUseTemplateScope() != null) client.setUseTemplateScope(resourceRep.isUseTemplateScope()); + else client.setUseTemplateScope(resourceRep.getClientTemplate() != null); + + if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers()); + else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null); + + return client; } @@ -949,14 +964,23 @@ public class RepresentationToModel { } } + if (rep.isUseTemplateConfig() != null) resource.setUseTemplateConfig(rep.isUseTemplateConfig()); + if (rep.isUseTemplateScope() != null) resource.setUseTemplateScope(rep.isUseTemplateScope()); + if (rep.isUseTemplateMappers() != null) resource.setUseTemplateMappers(rep.isUseTemplateMappers()); + + if (rep.getClientTemplate() != null) { if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) { resource.setClientTemplate(null); } else { RealmModel realm = resource.getRealm(); for (ClientTemplateModel template : realm.getClientTemplates()) { + if (template.getName().equals(rep.getClientTemplate())) { resource.setClientTemplate(template); + if (rep.isUseTemplateConfig() == null) resource.setUseTemplateConfig(true); + if (rep.isUseTemplateScope() == null) resource.setUseTemplateScope(true); + if (rep.isUseTemplateMappers() == null) resource.setUseTemplateMappers(true); break; } } @@ -984,7 +1008,7 @@ public class RepresentationToModel { if (resourceRep.getName() != null) client.setName(resourceRep.getName()); if(resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol()); - + if (resourceRep.isFullScopeAllowed() != null) client.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); if (resourceRep.getProtocolMappers() != null) { // first, remove all default/built in mappers Set mappers = client.getProtocolMappers(); @@ -1001,6 +1025,9 @@ public class RepresentationToModel { public static void updateClientTemplate(ClientTemplateRepresentation rep, ClientTemplateModel resource) { if (rep.getName() != null) resource.setName(rep.getName()); if (rep.getDescription() != null) resource.setDescription(rep.getDescription()); + if (rep.isFullScopeAllowed() != null) { + resource.setFullScopeAllowed(rep.isFullScopeAllowed()); + } if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index d96e6bcace..164fc74636 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -69,6 +69,47 @@ public class ClientAdapter implements ClientModel { } + @Override + public boolean useTemplateScope() { + if (updated != null) return updated.useTemplateScope(); + return cached.isUseTemplateScope(); + } + + @Override + public void setUseTemplateScope(boolean value) { + getDelegateForUpdate(); + updated.setUseTemplateScope(value); + + } + + @Override + public boolean useTemplateConfig() { + if (updated != null) return updated.useTemplateConfig(); + return cached.isUseTemplateConfig(); + } + + @Override + public void setUseTemplateConfig(boolean value) { + getDelegateForUpdate(); + updated.setUseTemplateConfig(value); + + } + + @Override + public boolean useTemplateMappers() { + if (updated != null) return updated.useTemplateMappers(); + return cached.isUseTemplateMappers(); + } + + @Override + public void setUseTemplateMappers(boolean value) { + getDelegateForUpdate(); + updated.setUseTemplateMappers(value); + + } + + + public void addWebOrigin(String webOrigin) { getDelegateForUpdate(); updated.addWebOrigin(webOrigin); @@ -412,25 +453,6 @@ public class ClientAdapter implements ClientModel { updated.updateDefaultRoles(defaultRoles); } - @Override - public Set getClientScopeMappings(ClientModel client) { - Set roleMappings = client.getScopeMappings(); - - Set appRoles = new HashSet(); - for (RoleModel role : roleMappings) { - RoleContainerModel container = role.getContainer(); - if (container instanceof RealmModel) { - } else { - ClientModel app = (ClientModel)container; - if (app.getId().equals(getId())) { - appRoles.add(role); - } - } - } - - return appRoles; - } - @Override public boolean isBearerOnly() { if (updated != null) return updated.isBearerOnly(); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java index 2a1674d9ad..13b68aa881 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java @@ -132,6 +132,70 @@ public class ClientTemplateAdapter implements ClientTemplateModel { updated.setProtocol(protocol); } + @Override + public boolean isFullScopeAllowed() { + if (updated != null) return updated.isFullScopeAllowed(); + return cached.isFullScopeAllowed(); + } + + @Override + public void setFullScopeAllowed(boolean value) { + getDelegateForUpdate(); + updated.setFullScopeAllowed(value); + + } + + public Set getScopeMappings() { + if (updated != null) return updated.getScopeMappings(); + Set roles = new HashSet(); + for (String id : cached.getScope()) { + roles.add(cacheSession.getRoleById(id, getRealm())); + + } + return roles; + } + + public void addScopeMapping(RoleModel role) { + getDelegateForUpdate(); + updated.addScopeMapping(role); + } + + public void deleteScopeMapping(RoleModel role) { + getDelegateForUpdate(); + updated.deleteScopeMapping(role); + } + + public Set getRealmScopeMappings() { + Set roleMappings = getScopeMappings(); + + Set appRoles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof RealmModel) { + if (((RealmModel) container).getId().equals(cachedRealm.getId())) { + appRoles.add(role); + } + } + } + + return appRoles; + } + + @Override + public boolean hasScope(RoleModel role) { + if (updated != null) return updated.hasScope(role); + if (cached.isFullScopeAllowed() || cached.getScope().contains(role.getId())) return true; + + Set roles = getScopeMappings(); + + for (RoleModel mapping : roles) { + if (mapping.hasRole(role)) return true; + } + return false; + } + + + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java index dbf47544ab..ab83077a27 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java @@ -56,6 +56,9 @@ public class CachedClient implements Serializable { private int nodeReRegistrationTimeout; private Map registeredNodes; private String clientTemplate; + private boolean useTemplateScope; + private boolean useTemplateConfig; + private boolean useTemplateMappers; public CachedClient(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) { id = model.getId(); @@ -102,6 +105,9 @@ public class CachedClient implements Serializable { if (model.getClientTemplate() != null) { clientTemplate = model.getClientTemplate().getId(); } + useTemplateConfig = model.useTemplateConfig(); + useTemplateMappers = model.useTemplateMappers(); + useTemplateScope = model.useTemplateScope(); } public String getId() { return id; @@ -238,4 +244,16 @@ public class CachedClient implements Serializable { public String getClientTemplate() { return clientTemplate; } + + public boolean isUseTemplateScope() { + return useTemplateScope; + } + + public boolean isUseTemplateConfig() { + return useTemplateConfig; + } + + public boolean isUseTemplateMappers() { + return useTemplateMappers; + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java index 2df28c161a..58bcc126cd 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java @@ -28,6 +28,8 @@ public class CachedClientTemplate implements Serializable { private String description; private String realm; private String protocol; + private boolean fullScopeAllowed; + private Set scope = new HashSet(); private Set protocolMappers = new HashSet(); public CachedClientTemplate(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientTemplateModel model) { @@ -36,9 +38,13 @@ public class CachedClientTemplate implements Serializable { description = model.getDescription(); this.realm = realm.getId(); protocol = model.getProtocol(); + fullScopeAllowed = model.isFullScopeAllowed(); for (ProtocolMapperModel mapper : model.getProtocolMappers()) { this.protocolMappers.add(mapper); } + for (RoleModel role : model.getScopeMappings()) { + scope.add(role.getId()); + } } public String getId() { return id; @@ -63,4 +69,12 @@ public class CachedClientTemplate implements Serializable { public String getProtocol() { return protocol; } + + public boolean isFullScopeAllowed() { + return fullScopeAllowed; + } + + public Set getScope() { + return scope; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index a8abd1137b..0d9eea4c7d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -206,7 +206,7 @@ public class ClientAdapter implements ClientModel { } @Override - public Set getRealmScopeMappings() { + public Set getRealmScopeMappings() { Set roleMappings = getScopeMappings(); Set appRoles = new HashSet<>(); @@ -238,7 +238,8 @@ public class ClientAdapter implements ClientModel { @Override public void addScopeMapping(RoleModel role) { - if (hasScope(role)) return; + Set roles = getScopeMappings(); + if (roles.contains(role)) return; ScopeMappingEntity entity = new ScopeMappingEntity(); entity.setClient(getEntity()); RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); @@ -319,6 +320,39 @@ public class ClientAdapter implements ClientModel { } + @Override + public boolean useTemplateScope() { + return entity.isUseTemplateScope(); + } + + @Override + public void setUseTemplateScope(boolean flag) { + entity.setUseTemplateScope(flag); + + } + + @Override + public boolean useTemplateMappers() { + return entity.isUseTemplateMappers(); + } + + @Override + public void setUseTemplateMappers(boolean flag) { + entity.setUseTemplateMappers(flag); + + } + + @Override + public boolean useTemplateConfig() { + return entity.isUseTemplateConfig(); + } + + @Override + public void setUseTemplateConfig(boolean flag) { + entity.setUseTemplateConfig(flag); + + } + public static boolean contains(String str, String[] array) { for (String s : array) { if (str.equals(s)) return true; @@ -604,6 +638,7 @@ public class ClientAdapter implements ClientModel { String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em); em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", role).executeUpdate(); em.createNamedQuery("deleteScopeMappingByRole").setParameter("role", role).executeUpdate(); + em.createNamedQuery("deleteTemplateScopeMappingByRole").setParameter("role", role).executeUpdate(); role.setClient(null); em.flush(); em.remove(role); @@ -641,28 +676,6 @@ public class ClientAdapter implements ClientModel { return false; } - @Override - public Set getClientScopeMappings(ClientModel client) { - Set roleMappings = client.getScopeMappings(); - - Set appRoles = new HashSet(); - for (RoleModel role : roleMappings) { - RoleContainerModel container = role.getContainer(); - if (container instanceof RealmModel) { - } else { - ClientModel app = (ClientModel)container; - if (app.getId().equals(getId())) { - appRoles.add(role); - } - } - } - - return appRoles; - } - - - - @Override public List getDefaultRoles() { Collection entities = entity.getDefaultRoles(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java index 8e92e79dc8..2d5c78b7c2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java @@ -14,6 +14,7 @@ import org.keycloak.models.jpa.entities.ClientTemplateEntity; import org.keycloak.models.jpa.entities.ProtocolMapperEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.ScopeMappingEntity; +import org.keycloak.models.jpa.entities.TemplateScopeMappingEntity; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; @@ -203,6 +204,88 @@ public class ClientTemplateAdapter implements ClientTemplateModel { return mapping; } + @Override + public boolean isFullScopeAllowed() { + return entity.isFullScopeAllowed(); + } + + @Override + public void setFullScopeAllowed(boolean value) { + entity.setFullScopeAllowed(value); + } + + @Override + public Set getRealmScopeMappings() { + Set roleMappings = getScopeMappings(); + + Set appRoles = new HashSet<>(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof RealmModel) { + if (((RealmModel) container).getId().equals(realm.getId())) { + appRoles.add(role); + } + } + } + + return appRoles; + } + + @Override + public Set getScopeMappings() { + TypedQuery query = em.createNamedQuery("clientTemplateScopeMappingIds", String.class); + query.setParameter("template", getEntity()); + List ids = query.getResultList(); + Set roles = new HashSet(); + for (String roleId : ids) { + RoleModel role = realm.getRoleById(roleId); + if (role == null) continue; + roles.add(role); + } + return roles; + } + + @Override + public void addScopeMapping(RoleModel role) { + if (hasScope(role)) return; + TemplateScopeMappingEntity entity = new TemplateScopeMappingEntity(); + entity.setTemplate(getEntity()); + RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); + entity.setRole(roleEntity); + em.persist(entity); + em.flush(); + em.detach(entity); + } + + @Override + public void deleteScopeMapping(RoleModel role) { + TypedQuery query = getRealmScopeMappingQuery(role); + List results = query.getResultList(); + if (results.size() == 0) return; + for (TemplateScopeMappingEntity entity : results) { + em.remove(entity); + } + } + + protected TypedQuery getRealmScopeMappingQuery(RoleModel role) { + TypedQuery query = em.createNamedQuery("templateHasScope", TemplateScopeMappingEntity.class); + query.setParameter("template", getEntity()); + RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); + query.setParameter("role", roleEntity); + return query; + } + + @Override + public boolean hasScope(RoleModel role) { + if (isFullScopeAllowed()) return true; + Set roles = getScopeMappings(); + if (roles.contains(role)) return true; + + for (RoleModel mapping : roles) { + if (mapping.hasRole(role)) return true; + } + return false; + } @Override public boolean equals(Object o) { @@ -219,4 +302,6 @@ public class ClientTemplateAdapter implements ClientTemplateModel { } + + } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 25df437f5b..25e79f8ae8 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -110,6 +110,9 @@ public class JpaRealmProvider implements RealmProvider { for (ClientEntity a : new LinkedList<>(realm.getClients())) { adapter.removeClient(a.getId()); } + for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) { + adapter.removeClientTemplate(a.getId()); + } em.remove(realm); 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 4f96829e61..ccc333985d 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 @@ -1051,6 +1051,7 @@ public class RealmAdapter implements RealmModel { String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em); em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate(); em.createNamedQuery("deleteScopeMappingByRole").setParameter("role", roleEntity).executeUpdate(); + em.createNamedQuery("deleteTemplateScopeMappingByRole").setParameter("role", roleEntity).executeUpdate(); em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate(); em.remove(roleEntity); @@ -2146,9 +2147,12 @@ public class RealmAdapter implements RealmModel { if (client == null) { return false; } + em.createNamedQuery("deleteTemplateScopeMappingByClient").setParameter("template", clientEntity).executeUpdate(); + em.flush(); em.remove(clientEntity); em.flush(); + return true; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 6767e7b20c..80781b8ac4 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -61,6 +61,15 @@ public class ClientEntity { @JoinColumn(name = "CLIENT_TEMPLATE_ID") protected ClientTemplateEntity clientTemplate; + @Column(name="USE_TEMPLATE_CONFIG") + private boolean useTemplateConfig; + + @Column(name="USE_TEMPLATE_SCOPE") + private boolean useTemplateScope; + + @Column(name="USE_TEMPLATE_MAPPERS") + private boolean useTemplateMappers; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "REALM_ID") protected RealmEntity realm; @@ -404,4 +413,28 @@ public class ClientEntity { public void setClientTemplate(ClientTemplateEntity clientTemplate) { this.clientTemplate = clientTemplate; } + + public boolean isUseTemplateConfig() { + return useTemplateConfig; + } + + public void setUseTemplateConfig(boolean useTemplateConfig) { + this.useTemplateConfig = useTemplateConfig; + } + + public boolean isUseTemplateScope() { + return useTemplateScope; + } + + public void setUseTemplateScope(boolean useTemplateScope) { + this.useTemplateScope = useTemplateScope; + } + + public boolean isUseTemplateMappers() { + return useTemplateMappers; + } + + public void setUseTemplateMappers(boolean useTemplateMappers) { + this.useTemplateMappers = useTemplateMappers; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java index ff4bd14c0e..5da01c4c57 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java @@ -45,6 +45,8 @@ public class ClientTemplateEntity { @Column(name="PROTOCOL") private String protocol; + @Column(name="FULL_SCOPE_ALLOWED") + private boolean fullScopeAllowed; public RealmEntity getRealm() { return realm; @@ -93,4 +95,12 @@ public class ClientTemplateEntity { public void setProtocol(String protocol) { this.protocol = protocol; } + + public boolean isFullScopeAllowed() { + return fullScopeAllowed; + } + + public void setFullScopeAllowed(boolean fullScopeAllowed) { + this.fullScopeAllowed = fullScopeAllowed; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/TemplateScopeMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/TemplateScopeMappingEntity.java new file mode 100755 index 0000000000..375fd0578c --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/TemplateScopeMappingEntity.java @@ -0,0 +1,99 @@ +package org.keycloak.models.jpa.entities; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@NamedQueries({ + @NamedQuery(name="templateHasScope", query="select m from TemplateScopeMappingEntity m where m.template = :template and m.role = :role"), + @NamedQuery(name="clientTemplateScopeMappings", query="select m from TemplateScopeMappingEntity m where m.template = :template"), + @NamedQuery(name="clientTemplateScopeMappingIds", query="select m.role.id from TemplateScopeMappingEntity m where m.template = :template"), + @NamedQuery(name="deleteTemplateScopeMappingByRole", query="delete from TemplateScopeMappingEntity where role = :role"), + @NamedQuery(name="deleteTemplateScopeMappingByClient", query="delete from TemplateScopeMappingEntity where template = :template") +}) +@Table(name="TEMPLATE_SCOPE_MAPPING") +@Entity +@IdClass(TemplateScopeMappingEntity.Key.class) +public class TemplateScopeMappingEntity { + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name = "TEMPLATE_ID") + protected ClientTemplateEntity template; + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name="ROLE_ID") + protected RoleEntity role; + + public ClientTemplateEntity getTemplate() { + return template; + } + + public void setTemplate(ClientTemplateEntity template) { + this.template = template; + } + + public RoleEntity getRole() { + return role; + } + + public void setRole(RoleEntity role) { + this.role = role; + } + + public static class Key implements Serializable { + + protected ClientTemplateEntity template; + + protected RoleEntity role; + + public Key() { + } + + public Key(ClientTemplateEntity template, RoleEntity role) { + this.template = template; + this.role = role; + } + + public ClientTemplateEntity getTemplate() { + return template; + } + + public RoleEntity getRole() { + return role; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (template != null ? !template.getId().equals(key.template != null ? key.template.getId() : null) : key.template != null) return false; + if (role != null ? !role.getId().equals(key.role != null ? key.role.getId() : null) : key.role != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = template != null ? template.getId().hashCode() : 0; + result = 31 * result + (role != null ? role.getId().hashCode() : 0); + return result; + } + } + +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index af448c4018..02ccea48f2 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -10,6 +10,7 @@ import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.ScopeContainerModel; import org.keycloak.models.entities.ProtocolMapperEntity; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; @@ -620,19 +621,6 @@ public class ClientAdapter extends AbstractMongoAdapter imple return false; } - @Override - public Set getClientScopeMappings(ClientModel client) { - Set result = new HashSet(); - List roles = MongoModelUtils.getAllScopesOfClient(client, invocationContext); - - for (MongoRoleEntity role : roles) { - if (getId().equals(role.getClientId())) { - result.add(new RoleAdapter(session, getRealm(), role, this, invocationContext)); - } - } - return result; - } - @Override public List getDefaultRoles() { return getMongoEntity().getDefaultRoles(); @@ -726,4 +714,41 @@ public class ClientAdapter extends AbstractMongoAdapter imple updateMongoEntity(); } + + @Override + public boolean useTemplateScope() { + return getMongoEntity().isUseTemplateScope(); + } + + @Override + public void setUseTemplateScope(boolean flag) { + getMongoEntity().setUseTemplateScope(flag); + updateMongoEntity(); + + } + + @Override + public boolean useTemplateMappers() { + return getMongoEntity().isUseTemplateMappers(); + } + + @Override + public void setUseTemplateMappers(boolean flag) { + getMongoEntity().setUseTemplateMappers(flag); + updateMongoEntity(); + + } + + @Override + public boolean useTemplateConfig() { + return getMongoEntity().isUseTemplateConfig(); + } + + @Override + public void setUseTemplateConfig(boolean flag) { + getMongoEntity().setUseTemplateConfig(flag); + updateMongoEntity(); + + } + } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java index ee19deae45..91725bffd0 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java @@ -209,7 +209,70 @@ public class ClientTemplateAdapter extends AbstractMongoAdapter getScopeMappings() { + Set result = new HashSet(); + List roles = MongoModelUtils.getAllScopesOfTemplate(this, invocationContext); + + for (MongoRoleEntity role : roles) { + if (realm.getId().equals(role.getRealmId())) { + result.add(new RoleAdapter(session, realm, role, realm, invocationContext)); + } else { + // Likely applicationRole, but we don't have this application yet + result.add(new RoleAdapter(session, realm, role, invocationContext)); + } + } + return result; + } + + @Override + public Set getRealmScopeMappings() { + Set allScopes = getScopeMappings(); + + // Filter to retrieve just realm roles TODO: Maybe improve to avoid filter programmatically... Maybe have separate fields for realmRoles and appRoles on user? + Set realmRoles = new HashSet(); + for (RoleModel role : allScopes) { + MongoRoleEntity roleEntity = ((RoleAdapter) role).getRole(); + + if (realm.getId().equals(roleEntity.getRealmId())) { + realmRoles.add(role); + } + } + return realmRoles; + } + + @Override + public void addScopeMapping(RoleModel role) { + getMongoStore().pushItemToList(this.getMongoEntity(), "scopeIds", role.getId(), true, invocationContext); + } + + @Override + public void deleteScopeMapping(RoleModel role) { + getMongoStore().pullItemFromList(this.getMongoEntity(), "scopeIds", role.getId(), invocationContext); + } + + @Override + public boolean hasScope(RoleModel role) { + if (isFullScopeAllowed()) return true; + Set roles = getScopeMappings(); + if (roles.contains(role)) return true; + + for (RoleModel mapping : roles) { + if (mapping.hasRole(role)) return true; + } + return false; + } @Override public boolean equals(Object o) { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java index eaf221698a..907b29d26c 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java @@ -4,12 +4,15 @@ import com.mongodb.DBObject; import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.entities.ClientEntity; +import org.keycloak.models.entities.ClientTemplateEntity; import org.keycloak.models.mongo.keycloak.adapters.ClientAdapter; +import org.keycloak.models.mongo.keycloak.adapters.ClientTemplateAdapter; import org.keycloak.models.mongo.keycloak.adapters.GroupAdapter; import org.keycloak.models.mongo.keycloak.adapters.UserAdapter; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; @@ -52,6 +55,19 @@ public class MongoModelUtils { return Collections.emptyList(); } + DBObject query = new QueryBuilder() + .and("_id").in(scopeIds) + .get(); + return invContext.getMongoStore().loadEntities(MongoRoleEntity.class, query, invContext); + } + public static List getAllScopesOfTemplate(ClientTemplateModel template, MongoStoreInvocationContext invContext) { + ClientTemplateEntity scopedEntity = ((ClientTemplateAdapter)template).getMongoEntity(); + List scopeIds = scopedEntity.getScopeIds(); + + if (scopeIds == null || scopeIds.isEmpty()) { + return Collections.emptyList(); + } + DBObject query = new QueryBuilder() .and("_id").in(scopeIds) .get(); 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 346832a38b..d1a83bb127 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -267,7 +267,7 @@ public class TokenManager { Set requestedProtocolMappers = new HashSet(); ClientTemplateModel clientTemplate = client.getClientTemplate(); - if (clientTemplate != null) { + if (clientTemplate != null && client.useTemplateMappers()) { for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) { if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) { requestedProtocolMappers.add(protocolMapper.getId()); @@ -322,14 +322,22 @@ public class TokenManager { } + ClientTemplateModel template = client.getClientTemplate(); - if (client.isFullScopeAllowed()) { + boolean useTemplateScope = template != null && client.useTemplateScope(); + + if ( (useTemplateScope && template.isFullScopeAllowed()) || (client.isFullScopeAllowed())) { + logger.debug("Using full scope for client"); requestedRoles = roleMappings; } else { - - Set scopeMappings = client.getScopeMappings(); + Set scopeMappings = new HashSet<>(); + if (useTemplateScope) { + logger.debug("Adding template scope mappings"); + scopeMappings.addAll(template.getScopeMappings()); + } scopeMappings.addAll(client.getRoles()); - + Set clientScopeMappings = client.getScopeMappings(); + scopeMappings.addAll(clientScopeMappings); for (RoleModel role : roleMappings) { for (RoleModel desiredRole : scopeMappings) { Set visited = new HashSet(); @@ -337,7 +345,6 @@ public class TokenManager { } } } - if (applyScopeParam) { Collection scopeParamRoles; if (scopeParam != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java index 509d485e31..53cc76b354 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java @@ -67,7 +67,7 @@ public class ClientTemplateResource { protected RealmModel realm; private RealmAuth auth; private AdminEventBuilder adminEvent; - protected ClientTemplateModel client; + protected ClientTemplateModel template; protected KeycloakSession session; @Context @@ -80,10 +80,10 @@ public class ClientTemplateResource { return keycloak; } - public ClientTemplateResource(RealmModel realm, RealmAuth auth, ClientTemplateModel clientModel, KeycloakSession session, AdminEventBuilder adminEvent) { + public ClientTemplateResource(RealmModel realm, RealmAuth auth, ClientTemplateModel template, KeycloakSession session, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; - this.client = clientModel; + this.template = template; this.session = session; this.adminEvent = adminEvent; @@ -92,11 +92,21 @@ public class ClientTemplateResource { @Path("protocol-mappers") public ProtocolMappersResource getProtocolMappers() { - ProtocolMappersResource mappers = new ProtocolMappersResource(client, auth, adminEvent); + ProtocolMappersResource mappers = new ProtocolMappersResource(template, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(mappers); return mappers; } + /** + * Base path for managing the scope mappings for the client + * + * @return + */ + @Path("scope-mappings") + public ScopeMappedResource getScopeMappedResource() { + return new ScopeMappedResource(realm, auth, template, session, adminEvent); + } + /** * Update the client template * @param rep @@ -108,7 +118,7 @@ public class ClientTemplateResource { auth.requireManage(); try { - RepresentationToModel.updateClientTemplate(rep, client); + RepresentationToModel.updateClientTemplate(rep, template); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); return Response.noContent().build(); } catch (ModelDuplicateException e) { @@ -127,7 +137,7 @@ public class ClientTemplateResource { @Produces(MediaType.APPLICATION_JSON) public ClientTemplateRepresentation getClient() { auth.requireView(); - return ModelToRepresentation.toRepresentation(client); + return ModelToRepresentation.toRepresentation(template); } /** @@ -138,7 +148,7 @@ public class ClientTemplateResource { @NoCache public void deleteClientTemplate() { auth.requireManage(); - realm.removeClientTemplate(client.getId()); + realm.removeClientTemplate(template.getId()); adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java index 44b355e037..b90cb0fef7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java @@ -7,6 +7,8 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.ScopeContainerModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -29,15 +31,15 @@ import java.util.Set; public class ScopeMappedClientResource { protected RealmModel realm; private RealmAuth auth; - protected ClientModel client; + protected ScopeContainerModel scopeContainer; protected KeycloakSession session; protected ClientModel scopedClient; protected AdminEventBuilder adminEvent; - public ScopeMappedClientResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, ClientModel scopedClient, AdminEventBuilder adminEvent) { + public ScopeMappedClientResource(RealmModel realm, RealmAuth auth, ScopeContainerModel scopeContainer, KeycloakSession session, ClientModel scopedClient, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; - this.client = client; + this.scopeContainer = scopeContainer; this.session = session; this.scopedClient = scopedClient; this.adminEvent = adminEvent; @@ -56,7 +58,7 @@ public class ScopeMappedClientResource { public List getClientScopeMappings() { auth.requireView(); - Set mappings = scopedClient.getClientScopeMappings(client); + Set mappings = KeycloakModelUtils.getClientScopeMappings(scopedClient, scopeContainer); //scopedClient.getClientScopeMappings(client); List mapRep = new ArrayList(); for (RoleModel roleModel : mappings) { mapRep.add(ModelToRepresentation.toRepresentation(roleModel)); @@ -79,7 +81,7 @@ public class ScopeMappedClientResource { auth.requireView(); Set roles = scopedClient.getRoles(); - return ScopeMappedResource.getAvailable(client, roles); + return ScopeMappedResource.getAvailable(scopeContainer, roles); } /** @@ -97,7 +99,7 @@ public class ScopeMappedClientResource { auth.requireView(); Set roles = scopedClient.getRoles(); - return ScopeMappedResource.getComposite(client, roles); + return ScopeMappedResource.getComposite(scopeContainer, roles); } /** @@ -115,7 +117,7 @@ public class ScopeMappedClientResource { if (roleModel == null) { throw new NotFoundException("Role not found"); } - client.addScopeMapping(roleModel); + scopeContainer.addScopeMapping(roleModel); adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), roleModel.getId()).representation(roles).success(); } } @@ -131,9 +133,9 @@ public class ScopeMappedClientResource { auth.requireManage(); if (roles == null) { - Set roleModels = scopedClient.getClientScopeMappings(client); + Set roleModels = KeycloakModelUtils.getClientScopeMappings(scopedClient, scopeContainer);//scopedClient.getClientScopeMappings(client); for (RoleModel roleModel : roleModels) { - client.deleteScopeMapping(roleModel); + scopeContainer.deleteScopeMapping(roleModel); } adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).representation(roles).success(); } else { @@ -142,7 +144,7 @@ public class ScopeMappedClientResource { if (roleModel == null) { throw new NotFoundException("Role not found"); } - client.deleteScopeMapping(roleModel); + scopeContainer.deleteScopeMapping(roleModel); adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri(), roleModel.getId()).representation(roles).success(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java index 2d9b6a263b..499392cfd2 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java @@ -7,6 +7,8 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.ScopeContainerModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ClientMappingsRepresentation; import org.keycloak.representations.idm.MappingsRepresentation; @@ -36,14 +38,14 @@ import java.util.Set; public class ScopeMappedResource { protected RealmModel realm; private RealmAuth auth; - protected ClientModel client; + protected ScopeContainerModel scopeContainer; protected KeycloakSession session; protected AdminEventBuilder adminEvent; - public ScopeMappedResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, AdminEventBuilder adminEvent) { + public ScopeMappedResource(RealmModel realm, RealmAuth auth, ScopeContainerModel scopeContainer, KeycloakSession session, AdminEventBuilder adminEvent) { this.realm = realm; this.auth = auth; - this.client = client; + this.scopeContainer = scopeContainer; this.session = session; this.adminEvent = adminEvent; } @@ -60,7 +62,7 @@ public class ScopeMappedResource { auth.requireView(); MappingsRepresentation all = new MappingsRepresentation(); - Set realmMappings = client.getRealmScopeMappings(); + Set realmMappings = scopeContainer.getRealmScopeMappings(); if (realmMappings.size() > 0) { List realmRep = new ArrayList(); for (RoleModel roleModel : realmMappings) { @@ -73,7 +75,7 @@ public class ScopeMappedResource { if (clients.size() > 0) { Map clientMappings = new HashMap(); for (ClientModel client : clients) { - Set roleMappings = client.getClientScopeMappings(this.client); + Set roleMappings = KeycloakModelUtils.getClientScopeMappings(client, this.scopeContainer); //client.getClientScopeMappings(this.client); if (roleMappings.size() > 0) { ClientMappingsRepresentation mappings = new ClientMappingsRepresentation(); mappings.setId(client.getId()); @@ -103,7 +105,7 @@ public class ScopeMappedResource { public List getRealmScopeMappings() { auth.requireView(); - Set realmMappings = client.getRealmScopeMappings(); + Set realmMappings = scopeContainer.getRealmScopeMappings(); List realmMappingsRep = new ArrayList(); for (RoleModel roleModel : realmMappings) { realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel)); @@ -124,10 +126,10 @@ public class ScopeMappedResource { auth.requireView(); Set roles = realm.getRoles(); - return getAvailable(client, roles); + return getAvailable(scopeContainer, roles); } - public static List getAvailable(ClientModel client, Set roles) { + public static List getAvailable(ScopeContainerModel client, Set roles) { List available = new ArrayList(); for (RoleModel roleModel : roles) { if (client.hasScope(roleModel)) continue; @@ -153,10 +155,10 @@ public class ScopeMappedResource { auth.requireView(); Set roles = realm.getRoles(); - return getComposite(client, roles); + return getComposite(scopeContainer, roles); } - public static List getComposite(ClientModel client, Set roles) { + public static List getComposite(ScopeContainerModel client, Set roles) { List composite = new ArrayList(); for (RoleModel roleModel : roles) { if (client.hasScope(roleModel)) composite.add(ModelToRepresentation.toRepresentation(roleModel)); @@ -180,7 +182,7 @@ public class ScopeMappedResource { if (roleModel == null) { throw new NotFoundException("Role not found"); } - client.addScopeMapping(roleModel); + scopeContainer.addScopeMapping(roleModel); adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), role.getId()).representation(roles).success(); } } @@ -197,9 +199,9 @@ public class ScopeMappedResource { auth.requireManage(); if (roles == null) { - Set roleModels = client.getRealmScopeMappings(); + Set roleModels = scopeContainer.getRealmScopeMappings(); for (RoleModel roleModel : roleModels) { - client.deleteScopeMapping(roleModel); + scopeContainer.deleteScopeMapping(roleModel); } adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).representation(roles).success(); } else { @@ -208,7 +210,7 @@ public class ScopeMappedResource { if (roleModel == null) { throw new NotFoundException("Client not found"); } - client.deleteScopeMapping(roleModel); + scopeContainer.deleteScopeMapping(roleModel); adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri(), roleModel.getId()).representation(roles).success(); } } @@ -221,6 +223,6 @@ public class ScopeMappedResource { if (clientModel == null) { throw new NotFoundException("Client not found"); } - return new ScopeMappedClientResource(realm, auth, this.client, session, clientModel, adminEvent); + return new ScopeMappedClientResource(realm, auth, this.scopeContainer, session, clientModel, adminEvent); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index d02c2559cc..ab76f89941 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -168,7 +168,7 @@ public class AccountTest { }); } - //@Test + @Test public void ideTesting() throws Exception { Thread.sleep(100000000); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index f016ff09f1..57afc834b1 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -25,6 +25,7 @@ import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; @@ -191,7 +192,7 @@ public class ImportTest extends AbstractModelTest { Set realmScopes = oauthClient.getRealmScopeMappings(); Assert.assertTrue(realmScopes.contains(realm.getRole("admin"))); - Set appScopes = application.getClientScopeMappings(oauthClient); + Set appScopes = KeycloakModelUtils.getClientScopeMappings(application, oauthClient);//application.getClientScopeMappings(oauthClient); Assert.assertTrue(appScopes.contains(application.getRole("app-user"))); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 8473540d73..5977916922 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -62,6 +62,8 @@ import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; @@ -803,6 +805,25 @@ public class AccessTokenTest { @Test public void testClientTemplate() throws Exception { RealmResource realm = keycloak.realms().realm("test"); + RoleRepresentation realmRole = new RoleRepresentation(); + realmRole.setName("realm-test-role"); + realm.roles().create(realmRole); + realmRole = realm.roles().get("realm-test-role").toRepresentation(); + RoleRepresentation realmRole2 = new RoleRepresentation(); + realmRole2.setName("realm-test-role2"); + realm.roles().create(realmRole2); + realmRole2 = realm.roles().get("realm-test-role2").toRepresentation(); + + + List users = realm.users().search("test-user@localhost", -1, -1); + Assert.assertEquals(1, users.size()); + UserRepresentation user = users.get(0); + + List addRoles = new LinkedList<>(); + addRoles.add(realmRole); + addRoles.add(realmRole2); + realm.users().get(user.getId()).roles().realmLevel().add(addRoles); + ClientTemplateRepresentation rep = new ClientTemplateRepresentation(); rep.setName("template"); rep.setProtocol("oidc"); @@ -825,8 +846,8 @@ public class AccessTokenTest { } } - Assert.assertNotNull(clientRep); clientRep.setClientTemplate("template"); + clientRep.setFullScopeAllowed(false); realm.clients().get(clientRep.getId()).update(clientRep); { @@ -844,13 +865,150 @@ public class AccessTokenTest { AccessToken accessToken = getAccessToken(tokenResponse); Assert.assertEquals("coded", accessToken.getOtherClaims().get("hard")); + // check zero scope for template + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole.getName())); + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole2.getName())); + response.close(); client.close(); } + + // test that scope is added + List addRole1 = new LinkedList<>(); + addRole1.add(realmRole); + templateResource.getScopeMappings().realmLevel().add(addRole1); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + AccessToken accessToken = getAccessToken(tokenResponse); + // check zero scope for template + Assert.assertNotNull(accessToken.getRealmAccess()); + Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains(realmRole.getName())); + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole2.getName())); + + + response.close(); + client.close(); + } + + // test combined scopes + List addRole2 = new LinkedList<>(); + addRole2.add(realmRole2); + realm.clients().get(clientRep.getId()).getScopeMappings().realmLevel().add(addRole2); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + + AccessToken accessToken = getAccessToken(tokenResponse); + + // check zero scope for template + Assert.assertNotNull(accessToken.getRealmAccess()); + Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains(realmRole.getName())); + Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains(realmRole2.getName())); + + + response.close(); + client.close(); + } + + // remove scopes and retest + templateResource.getScopeMappings().realmLevel().remove(addRole1); + realm.clients().get(clientRep.getId()).getScopeMappings().realmLevel().remove(addRole2); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + + AccessToken accessToken = getAccessToken(tokenResponse); + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole.getName())); + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole2.getName())); + + + response.close(); + client.close(); + } + + // test full scope on template + rep.setFullScopeAllowed(true); + templateResource.update(rep); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + + AccessToken accessToken = getAccessToken(tokenResponse); + + // check zero scope for template + Assert.assertNotNull(accessToken.getRealmAccess()); + Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains(realmRole.getName())); + Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains(realmRole2.getName())); + + + response.close(); + client.close(); + } + + // test don't use template scope + clientRep.setUseTemplateScope(false); + realm.clients().get(clientRep.getId()).update(clientRep); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + + AccessToken accessToken = getAccessToken(tokenResponse); + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole.getName())); + Assert.assertFalse(accessToken.getRealmAccess().getRoles().contains(realmRole2.getName())); + + + response.close(); + client.close(); + } + + + + // undo mappers clientRep.setClientTemplate(ClientTemplateRepresentation.NONE); + clientRep.setFullScopeAllowed(true); realm.clients().get(clientRep.getId()).update(clientRep); + realm.users().get(user.getId()).roles().realmLevel().remove(addRoles); + realm.roles().get(realmRole.getName()).remove(); + realm.roles().get(realmRole2.getName()).remove(); + templateResource.remove(); { Client client = ClientBuilder.newClient(); From a7c684b909168e444d2ff1cad0bc0c52c38c9828 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 18 Dec 2015 17:19:31 -0500 Subject: [PATCH 17/65] oops --- .../test/java/org/keycloak/testsuite/account/AccountTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index ab76f89941..d02c2559cc 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -168,7 +168,7 @@ public class AccountTest { }); } - @Test + //@Test public void ideTesting() throws Exception { Thread.sleep(100000000); } From 86a0995f29dac53ea5964953e37afa96e9be07d8 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 18 Dec 2015 17:31:53 -0500 Subject: [PATCH 18/65] fix test --- .../java/org/keycloak/testsuite/oauth/AccessTokenTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 5977916922..4e7629a2ce 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -848,6 +848,9 @@ public class AccessTokenTest { } clientRep.setClientTemplate("template"); clientRep.setFullScopeAllowed(false); + clientRep.setUseTemplateMappers(true); + clientRep.setUseTemplateScope(true); + clientRep.setUseTemplateConfig(true); realm.clients().get(clientRep.getId()).update(clientRep); { From 606e6fa479eb9e0f28c66606c104c05d267fa9e8 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Dec 2015 11:08:12 +0100 Subject: [PATCH 19/65] KEYCLOAK-1934 Add display-name and display-name-html to realm --- .../java/org/keycloak/common/Version.java | 4 +++ .../resources/keycloak-version.properties | 2 ++ .../idm/RealmRepresentation.java | 18 ++++++++++ .../messages/admin-messages_en.properties | 2 ++ .../resources/partials/realm-detail.html | 14 ++++++++ .../theme/base/login/login-idp-link-email.ftl | 2 +- .../theme/base/login/login-oauth-grant.ftl | 2 +- .../resources/theme/base/login/login-totp.ftl | 4 +-- .../main/resources/theme/base/login/login.ftl | 4 +-- .../login/messages/messages_ca.properties | 6 ++-- .../login/messages/messages_de.properties | 6 ++-- .../login/messages/messages_en.properties | 8 ++--- .../login/messages/messages_es.properties | 6 ++-- .../login/messages/messages_fr.properties | 6 ++-- .../login/messages/messages_it.properties | 6 ++-- .../login/messages/messages_pt_BR.properties | 6 ++-- .../resources/theme/base/login/register.ftl | 4 +-- .../login/freemarker/model/RealmBean.java | 18 ++++++++++ .../java/org/keycloak/models/RealmModel.java | 8 +++++ .../keycloak/models/entities/RealmEntity.java | 18 ++++++++++ .../models/utils/ModelToRepresentation.java | 2 ++ .../models/utils/RepresentationToModel.java | 4 +++ .../models/cache/infinispan/RealmAdapter.java | 24 +++++++++++++ .../models/cache/entities/CachedRealm.java | 12 +++++++ .../org/keycloak/models/jpa/RealmAdapter.java | 36 +++++++++++-------- .../models/jpa/entities/RealmAttributes.java | 12 +++++++ .../mongo/keycloak/adapters/RealmAdapter.java | 22 ++++++++++++ pom.xml | 1 + .../services/managers/ApplianceBootstrap.java | 3 ++ .../testsuite/pages/OAuthGrantPage.java | 2 +- 30 files changed, 216 insertions(+), 46 deletions(-) create mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java diff --git a/common/src/main/java/org/keycloak/common/Version.java b/common/src/main/java/org/keycloak/common/Version.java index 73c4959665..895eda5b4c 100755 --- a/common/src/main/java/org/keycloak/common/Version.java +++ b/common/src/main/java/org/keycloak/common/Version.java @@ -12,6 +12,8 @@ import java.util.Properties; */ public class Version { public static final String UNKNOWN = "UNKNOWN"; + public static String NAME; + public static String NAME_HTML; public static String VERSION; public static String RESOURCES_VERSION; public static String BUILD_TIME; @@ -21,6 +23,8 @@ public class Version { InputStream is = Version.class.getResourceAsStream("/keycloak-version.properties"); try { props.load(is); + Version.NAME = props.getProperty("name"); + Version.NAME_HTML = props.getProperty("name-html"); Version.VERSION = props.getProperty("version"); Version.BUILD_TIME = props.getProperty("build-time"); Version.RESOURCES_VERSION = Version.VERSION.toLowerCase(); diff --git a/common/src/main/resources/keycloak-version.properties b/common/src/main/resources/keycloak-version.properties index fa367b82b5..7ef5089ebe 100755 --- a/common/src/main/resources/keycloak-version.properties +++ b/common/src/main/resources/keycloak-version.properties @@ -1,2 +1,4 @@ +name=${product.name} +name-html=${product.name-html} version=${product.version} build-time=${product.build-time} \ No newline at end of file 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 cc57124991..3359f2195f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -9,6 +9,8 @@ import java.util.*; public class RealmRepresentation { protected String id; protected String realm; + protected String displayName; + protected String displayNameHtml; protected Integer notBefore; protected Boolean revokeRefreshToken; protected Integer accessTokenLifespan; @@ -129,6 +131,22 @@ public class RealmRepresentation { this.realm = realm; } + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDisplayNameHtml() { + return displayNameHtml; + } + + public void setDisplayNameHtml(String displayNameHtml) { + this.displayNameHtml = displayNameHtml; + } + public List getUsers() { return users; } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index f33d6b835a..ca128ccaa8 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -1,6 +1,8 @@ # Common messages enabled=Enabled name=Name +displayName=Display name +displayNameHtml=HTML Display name save=Save cancel=Cancel onText=ON diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html index d39ba1403b..19561ea5d6 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html @@ -9,6 +9,20 @@
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl index 0ba068603c..5dc29f1c11 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl @@ -6,7 +6,7 @@ ${msg("emailLinkIdpTitle", idpAlias)} <#elseif section = "form">

    - ${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.name)} + ${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}

    ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")} diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl index 2d596d09e4..edafc660e0 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-oauth-grant.ftl @@ -3,7 +3,7 @@ <#if section = "title"> ${msg("oauthGrantTitle")} <#elseif section = "header"> - ${msg("oauthGrantTitleHtml",(realm.name!''))} <#if client.name??>${advancedMsg(client.name)}<#else>${client.clientId}. + ${msg("oauthGrantTitleHtml",(realm.displayNameHtml!''))} <#if client.name??>${advancedMsg(client.name)}<#else>${client.clientId}. <#elseif section = "form">

    ${msg("oauthGrantRequest")}

    diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl index 3f46b76ba1..12cda214ec 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout; section> <#if section = "title"> - ${msg("loginTitle",realm.name)} + ${msg("loginTitle",realm.displayName)} <#elseif section = "header"> - ${msg("loginTitleHtml",realm.name)} + ${msg("loginTitleHtml",realm.displayNameHtml)} <#elseif section = "form">
    diff --git a/forms/common-themes/src/main/resources/theme/base/login/login.ftl b/forms/common-themes/src/main/resources/theme/base/login/login.ftl index 5786807722..840bf4e135 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=social.displayInfo; section> <#if section = "title"> - ${msg("loginTitle",(realm.name!''))} + ${msg("loginTitle",(realm.displayName!''))} <#elseif section = "header"> - ${msg("loginTitleHtml",(realm.name!''))} + ${msg("loginTitleHtml",(realm.displayNameHtml!''))} <#elseif section = "form"> <#if realm.password> diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties index 55ccb7b24f..720e73ee6d 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties @@ -15,9 +15,9 @@ kerberosNotConfiguredTitle=Kerberos no configurat bypassKerberosDetail=O b\u00E9 no est\u00E0s identificat mitjan\u00E7ant Kerberos o el teu navegador no est\u00E0 configurat per identificar-se mitjan\u00E7ant Kerberos. Si us plau fes clic per identificar-te per un altre mitj\u00E0. kerberosNotSetUp=Kerberos no est\u00E0 configurat. No pots identificar-te. registerWithTitle=Registra''t amb {0} -registerWithTitleHtml=Registra''t amb {0} +registerWithTitleHtml={0} loginTitle=Inicia sessi\u00F3 a {0} -loginTitleHtml=Inicia sessi\u00F3 a {0} +loginTitleHtml={0} impersonateTitle={0}\u00A0Personifica Usuari impersonateTitleHtml={0} Personifica Usuari realmChoice=Domini @@ -26,7 +26,7 @@ loginTotpTitle=Configura la teva aplicaci\u00F3 d''identificaci\u00F3 m\u00F2bil loginProfileTitle=Actualitza la informaci\u00F3 del teu compte loginTimeout=Has trigat massa a identificar-te. Inicia de nou la identificaci\u00F3. oauthGrantTitle=Concessi\u00F3 OAuth -oauthGrantTitleHtml=Acc\u00E9s temporal per {0} sol\u00B7licitat per +oauthGrantTitleHtml={0} errorTitle=Ho sentim... errorTitleHtml=Ho sentim... emailVerifyTitle=Verificaci\u00F3 de l''email diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties index d968379dcc..2d96a2c495 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties @@ -19,9 +19,9 @@ recaptchaNotConfigured=Recaptcha is required, but not configured consentDenied=Consent denied. registerWithTitle=Registrierung bei {0} -registerWithTitleHtml=Registrierung bei {0} +registerWithTitleHtml={0} loginTitle=Anmeldung bei {0} -loginTitleHtml=Anmeldung bei {0} +loginTitleHtml={0} loginOauthTitle= loginOauthTitleHtml=Tempor\u00E4rer zugriff auf {0} angefordert von {1}. loginTotpTitle=Mobile Authentifizierung Einrichten @@ -32,7 +32,7 @@ impersonateTitleHtml={0} Impersonate User unknownUser=Unknown user realmChoice=Realm oauthGrantTitle=OAuth gew\u00E4hren -oauthGrantTitleHtml=Tempor\u00E4rer zugriff auf {0} angefordert von +oauthGrantTitleHtml={0} errorTitle=Es tut uns leid... errorTitleHtml=Es tut uns leid... emailVerifyTitle=E-Mail verifizieren diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties index b1b5890d6e..4b5df27367 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -15,9 +15,9 @@ kerberosNotConfiguredTitle=Kerberos Not Configured bypassKerberosDetail=Either you are not logged in via Kerberos or your browser is not set up for Kerberos login. Please click continue to login in through other means kerberosNotSetUp=Kerberos is not set up. You cannot login. registerWithTitle=Register with {0} -registerWithTitleHtml=Register with {0} +registerWithTitleHtml={0} loginTitle=Log in to {0} -loginTitleHtml=Log in to {0} +loginTitleHtml={0} impersonateTitle={0} Impersonate User impersonateTitleHtml={0} Impersonate User realmChoice=Realm @@ -25,8 +25,8 @@ unknownUser=Unknown user loginTotpTitle=Mobile Authenticator Setup loginProfileTitle=Update Account Information loginTimeout=You took too long to login. Login process starting from beginning. -oauthGrantTitle=OAuth Grant -oauthGrantTitleHtml=Temporary access for {0} requested by +oauthGrantTitle=Grant Access +oauthGrantTitleHtml={0} errorTitle=We''re sorry... errorTitleHtml=We''re sorry ... emailVerifyTitle=Email verification diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties index f497d07ce7..a42f7d2480 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties @@ -15,9 +15,9 @@ kerberosNotConfiguredTitle=Kerberos no configurado bypassKerberosDetail=O bien no est\u00E1s identificado mediante Kerberos o tu navegador no est\u00E1 configurado para identificarse mediante Kerberos. Por favor haz clic para identificarte por otro medio. kerberosNotSetUp=Kerberos no est\u00E1 configurado. No puedes identificarte. registerWithTitle=Reg\u00EDstrate con {0} -registerWithTitleHtml=Reg\u00EDstrate con {0} +registerWithTitleHtml={0} loginTitle=Inicia sesi\u00F3n en {0} -loginTitleHtml=Inicia sesi\u00F3n en {0} +loginTitleHtml={0} impersonateTitle={0}\u00A0Personificar Usuario impersonateTitleHtml={0} Personificar Usuario realmChoice=Dominio @@ -26,7 +26,7 @@ loginTotpTitle=Configura tu aplicaci\u00F3n de identificaci\u00F3n m\u00F3vil loginProfileTitle=Actualiza la informaci\u00F3n de tu cuenta loginTimeout=Has tardado demasiado en identificarte. Inicia de nuevo la identificaci\u00F3n. oauthGrantTitle=Concesi\u00F3n OAuth -oauthGrantTitleHtml=Acceso temporal para {0} solicitado por +oauthGrantTitleHtml={0} errorTitle=Lo sentimos... errorTitleHtml=Lo sentimos... emailVerifyTitle=Verificaci\u00F3n del email diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties index 495dde695d..662e9d940a 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties @@ -15,9 +15,9 @@ kerberosNotConfiguredTitle=Kerberos non configur\u00e9 bypassKerberosDetail=Si vous n''\u00eates pas connect\u00e9 via Kerberos ou bien que votre navigateur n''est pas configur\u00e9 pour la connexion via Kerberos. Veuillez cliquer pour vous connecter via un autre moyen. kerberosNotSetUp=Kerberos n''est pas configur\u00e9. Connexion impossible. registerWithTitle=Enregistrement avec {0} -registerWithTitleHtml=Enregistrement avec {0} +registerWithTitleHtml={0} loginTitle=Se connecter \u00e0 {0} -loginTitleHtml=Se connecter \u00e0 {0} +loginTitleHtml={0} impersonateTitle={0} utilisateur impersonate impersonateTitleHtml={0} utilisateur impersonate realmChoice=Domaine @@ -26,7 +26,7 @@ loginTotpTitle=Configuration de l''authentification par mobile loginProfileTitle=Mise \u00e0 jour du compte loginTimeout=Le temps imparti pour la connexion est \u00e9coul\u00e9. Le processus de connexion red\u00e9marre depuis le d\u00e9but. oauthGrantTitle=OAuth Grant -oauthGrantTitleHtml=Acc\u00e8s temporaire pour {0} demand\u00e9 par +oauthGrantTitleHtml={0} errorTitle=Nous sommes d\u00e9sol\u00e9 ... errorTitleHtml=Nous sommes d\u00e9sol\u00e9 ... emailVerifyTitle=V\u00e9rification du courriel diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties index 026c7a4fa2..0fd7d99d0e 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties @@ -19,9 +19,9 @@ recaptchaNotConfigured=Recaptcha is required, but not configured consentDenied=Consent denied. registerWithTitle=Registrati come {0} -registerWithTitleHtml=Registrati come {0} +registerWithTitleHtml={0} loginTitle=Accedi a {0} -loginTitleHtml=Accedi a {0} +loginTitleHtml={0} loginTotpTitle=Configura Autenticazione Mobile loginProfileTitle=Aggiorna Profilo loginTimeout=You took too long to login. Login process starting from beginning. @@ -30,7 +30,7 @@ impersonateTitleHtml={0} Impersonate User unknownUser=Unknown user realmChoice=Realm oauthGrantTitle=OAuth Grant -oauthGrantTitleHtml=Accesso temporaneo per {0} richiesto da +oauthGrantTitleHtml={0} errorTitle=Siamo spiacenti... errorTitleHtml=Siamo spiacenti ... emailVerifyTitle=Verifica Email diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties index 7c44eeb89f..a1441202bd 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -15,9 +15,9 @@ kerberosNotConfiguredTitle=Kerberos N\u00E3o Configurado bypassKerberosDetail=Ou voc\u00EA n\u00E3o est\u00E1 logado via Kerberos ou o seu navegador n\u00E3o est\u00E1 configurado para login Kerberos. Por favor, clique em continuar para fazer o login no atrav\u00E9s de outros meios kerberosNotSetUp=Kerberos n\u00E3o est\u00E1 configurado. Voc\u00EA n\u00E3o pode acessar. registerWithTitle=Registre-se com {0} -registerWithTitleHtml=Registre-se com {0} +registerWithTitleHtml={0} loginTitle=Entrar em {0} -loginTitleHtml=Entrar em {0} +loginTitleHtml={0} impersonateTitle={0} Impersonate User impersonateTitleHtml={0} Impersonate User realmChoice=Realm @@ -26,7 +26,7 @@ loginTotpTitle=Configura\u00E7\u00E3o do autenticador mobile loginProfileTitle=Atualiza\u00E7\u00E3o das Informa\u00E7\u00F5es da Conta loginTimeout=Voc\u00EA demorou muito para entrar. Por favor, refa\u00E7a o processo de login a partir do in\u00EDcio. oauthGrantTitle=Concess\u00E3o OAuth -oauthGrantTitleHtml=Acesso tempor\u00E1rio para {0} solicitado pela +oauthGrantTitleHtml={0} errorTitle=N\u00F3s lamentamos... errorTitleHtml=N\u00F3s lamentamos ... emailVerifyTitle=Verifica\u00E7\u00E3o de e-mail diff --git a/forms/common-themes/src/main/resources/theme/base/login/register.ftl b/forms/common-themes/src/main/resources/theme/base/login/register.ftl index d1593daedf..18a01cff17 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/register.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/register.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout; section> <#if section = "title"> - ${msg("registerWithTitle",(realm.name!''))} + ${msg("registerWithTitle",(realm.displayName!''))} <#elseif section = "header"> - ${msg("registerWithTitleHtml",(realm.name!''))} + ${msg("registerWithTitleHtml",(realm.displayNameHtml!''))} <#elseif section = "form"> <#if !realm.registrationEmailAsUsername> diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java index e6ae21de98..027a2c2b0b 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java @@ -42,6 +42,24 @@ public class RealmBean { return realm.getName(); } + public String getDisplayName() { + String displayName = realm.getDisplayName(); + if (displayName != null && displayName.length() > 0) { + return displayName; + } else { + return getName(); + } + } + + public String getDisplayNameHtml() { + String displayNameHtml = realm.getDisplayNameHtml(); + if (displayNameHtml != null && displayNameHtml.length() > 0) { + return displayNameHtml; + } else { + return getDisplayName(); + } + } + public boolean isIdentityFederationEnabled() { return realm.isIdentityFederationEnabled(); } diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 3e50b737d5..c0aa05f077 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -35,6 +35,14 @@ public interface RealmModel extends RoleContainerModel { void setName(String name); + String getDisplayName(); + + void setDisplayName(String displayName); + + String getDisplayNameHtml(); + + void setDisplayNameHtml(String displayNameHtml); + boolean isEnabled(); void setEnabled(boolean enabled); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 7a08bca7fb..69fdeaef15 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -11,6 +11,8 @@ import java.util.Map; public class RealmEntity extends AbstractIdentifiableEntity { private String name; + private String displayName; + private String displayNameHtml; private boolean enabled; private String sslRequired; private boolean registrationAllowed; @@ -105,6 +107,22 @@ public class RealmEntity extends AbstractIdentifiableEntity { this.name = name; } + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDisplayNameHtml() { + return displayNameHtml; + } + + public void setDisplayNameHtml(String displayNameHtml) { + this.displayNameHtml = displayNameHtml; + } + public boolean isEnabled() { return enabled; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index e891582a63..a18d0f8097 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -164,6 +164,8 @@ public class ModelToRepresentation { RealmRepresentation rep = new RealmRepresentation(); rep.setId(realm.getId()); rep.setRealm(realm.getName()); + rep.setDisplayName(realm.getDisplayName()); + rep.setDisplayNameHtml(realm.getDisplayNameHtml()); rep.setEnabled(realm.isEnabled()); rep.setNotBefore(realm.getNotBefore()); rep.setSslRequired(realm.getSslRequired().name().toLowerCase()); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 2d99c3b2a3..2432fc1274 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -85,6 +85,8 @@ public class RepresentationToModel { convertDeprecatedApplications(session, rep); newRealm.setName(rep.getRealm()); + if (rep.getDisplayName() != null) newRealm.setDisplayName(rep.getDisplayName()); + if (rep.getDisplayNameHtml() != null) newRealm.setDisplayNameHtml(rep.getDisplayNameHtml()); if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled()); if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected()); if (rep.getMaxFailureWaitSeconds() != null) newRealm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds()); @@ -595,6 +597,8 @@ public class RepresentationToModel { if (rep.getRealm() != null) { realm.setName(rep.getRealm()); } + if (rep.getDisplayName() != null) realm.setDisplayName(rep.getDisplayName()); + if (rep.getDisplayNameHtml() != null) realm.setDisplayNameHtml(rep.getDisplayNameHtml()); if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled()); if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected()); if (rep.getMaxFailureWaitSeconds() != null) realm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds()); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index e7d15eb862..77ac4306e7 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -59,6 +59,30 @@ public class RealmAdapter implements RealmModel { updated.setName(name); } + @Override + public String getDisplayName() { + if (updated != null) return updated.getDisplayName(); + return cached.getDisplayName(); + } + + @Override + public void setDisplayName(String displayName) { + getDelegateForUpdate(); + updated.setDisplayName(displayName); + } + + @Override + public String getDisplayNameHtml() { + if (updated != null) return updated.getDisplayNameHtml(); + return cached.getDisplayNameHtml(); + } + + @Override + public void setDisplayNameHtml(String displayNameHtml) { + getDelegateForUpdate(); + updated.setDisplayNameHtml(displayNameHtml); + } + @Override public boolean isEnabled() { if (updated != null) return updated.isEnabled(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index 40e4851f94..5b0be489bb 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -38,6 +38,8 @@ public class CachedRealm implements Serializable { private String id; private String name; + private String displayName; + private String displayNameHtml; private boolean enabled; private SslRequired sslRequired; private boolean registrationAllowed; @@ -125,6 +127,8 @@ public class CachedRealm implements Serializable { public CachedRealm(RealmCache cache, RealmProvider delegate, RealmModel model) { id = model.getId(); name = model.getName(); + displayName = model.getDisplayName(); + displayNameHtml = model.getDisplayNameHtml(); enabled = model.isEnabled(); sslRequired = model.getSslRequired(); registrationAllowed = model.isRegistrationAllowed(); @@ -265,6 +269,14 @@ public class CachedRealm implements Serializable { return name; } + public String getDisplayName() { + return displayName; + } + + public String getDisplayNameHtml() { + return displayNameHtml; + } + public List getDefaultRoles() { return defaultRoles; } 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 1530b12f98..ce72bbac04 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 @@ -21,21 +21,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderCreationEventImpl; import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity; -import org.keycloak.models.jpa.entities.AuthenticationFlowEntity; -import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity; -import org.keycloak.models.jpa.entities.ClientEntity; -import org.keycloak.models.jpa.entities.ClientTemplateEntity; -import org.keycloak.models.jpa.entities.GroupEntity; -import org.keycloak.models.jpa.entities.IdentityProviderEntity; -import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity; -import org.keycloak.models.jpa.entities.RealmAttributeEntity; -import org.keycloak.models.jpa.entities.RealmEntity; -import org.keycloak.models.jpa.entities.RequiredActionProviderEntity; -import org.keycloak.models.jpa.entities.RequiredCredentialEntity; -import org.keycloak.models.jpa.entities.RoleEntity; -import org.keycloak.models.jpa.entities.UserFederationMapperEntity; -import org.keycloak.models.jpa.entities.UserFederationProviderEntity; +import org.keycloak.models.jpa.entities.*; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; @@ -98,6 +84,26 @@ public class RealmAdapter implements RealmModel { em.flush(); } + @Override + public String getDisplayName() { + return getAttribute(RealmAttributes.DISPLAY_NAME); + } + + @Override + public void setDisplayName(String displayName) { + setAttribute(RealmAttributes.DISPLAY_NAME, displayName); + } + + @Override + public String getDisplayNameHtml() { + return getAttribute(RealmAttributes.DISPLAY_NAME_HTML); + } + + @Override + public void setDisplayNameHtml(String displayNameHtml) { + setAttribute(RealmAttributes.DISPLAY_NAME_HTML, displayNameHtml); + } + @Override public boolean isEnabled() { return realm.isEnabled(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java new file mode 100644 index 0000000000..ecc8768dca --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java @@ -0,0 +1,12 @@ +package org.keycloak.models.jpa.entities; + +/** + * @author Stian Thorgersen + */ +public interface RealmAttributes { + + String DISPLAY_NAME = "displayName"; + + String DISPLAY_NAME_HTML = "displayNameHtml"; + +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 6a1a84d0bf..707c9535d0 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -97,6 +97,28 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + @Override + public String getDisplayName() { + return realm.getDisplayName(); + } + + @Override + public void setDisplayName(String displayName) { + realm.setDisplayName(displayName); + updateRealm(); + } + + @Override + public String getDisplayNameHtml() { + return realm.getDisplayNameHtml(); + } + + @Override + public void setDisplayNameHtml(String displayNameHtml) { + realm.setDisplayNameHtml(displayNameHtml); + updateRealm(); + } + @Override public boolean isEnabled() { return realm.isEnabled(); diff --git a/pom.xml b/pom.xml index b2038aaf6d..b2aa2f416b 100755 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ Keycloak + \u003Cstrong\u003EKeycloak\u003C\u002Fstrong\u003E ${project.version} ${timestamp} diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index a868aa8fee..84020166d7 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -2,6 +2,7 @@ package org.keycloak.services.managers; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.common.Version; import org.keycloak.common.enums.SslRequired; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; @@ -39,6 +40,8 @@ public class ApplianceBootstrap { manager.setContextPath(contextPath); RealmModel realm = manager.createRealm(adminRealmName, adminRealmName); realm.setName(adminRealmName); + realm.setDisplayName(Version.NAME); + realm.setDisplayNameHtml(Version.NAME_HTML); realm.setEnabled(true); realm.addRequiredCredential(CredentialRepresentation.PASSWORD); realm.setSsoSessionIdleTimeout(1800); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java index baa1ae9a73..660beb0c07 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java @@ -45,7 +45,7 @@ public class OAuthGrantPage extends AbstractPage { @Override public boolean isCurrent() { - return driver.getTitle().equals("OAuth Grant"); + return driver.getTitle().equals("Grant Access"); } @Override From 1594e076288f095d5c63cb21206dfc735457971f Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Dec 2015 12:15:36 +0100 Subject: [PATCH 20/65] KEYCLOAK-2239 Add link to keycloak.org to Keycloak logo --- .../src/main/resources/theme/base/login/template.ftl | 2 +- .../theme/keycloak/login/resources/css/login.css | 11 ++++++----- .../resources/theme/keycloak/login/theme.properties | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/login/template.ftl b/forms/common-themes/src/main/resources/theme/base/login/template.ftl index aca358fd4c..a637969419 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/template.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/template.ftl @@ -29,7 +29,7 @@ - +
    diff --git a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css index 8f09a97946..f951c1d3eb 100644 --- a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css +++ b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css @@ -74,11 +74,12 @@ #kc-logo-wrapper { background-image: url("../img/keycloak-logo.png"); background-repeat: no-repeat; - background-position: top right; + position: absolute; + top: 50px; + right: 50px; height: 37px; - - margin: 50px; + width: 154px; } #kc-header { @@ -281,8 +282,8 @@ ol#kc-totp-settings li:first-of-type { @media (max-width: 767px) { #kc-logo-wrapper { - margin-top: 30px; - margin-right: 15px; + top: 15px; + right: 15px; } #kc-header { diff --git a/forms/common-themes/src/main/resources/theme/keycloak/login/theme.properties b/forms/common-themes/src/main/resources/theme/keycloak/login/theme.properties index 7319690b4b..b2364e1427 100644 --- a/forms/common-themes/src/main/resources/theme/keycloak/login/theme.properties +++ b/forms/common-themes/src/main/resources/theme/keycloak/login/theme.properties @@ -6,6 +6,8 @@ meta=viewport==width=device-width,initial-scale=1 kcHtmlClass=login-pf +kcLogoLink=http://www.keycloak.org + kcContentClass=col-sm-12 col-md-12 col-lg-12 container kcContentWrapperClass=row From 39904548ebbc9acf9a65b4161383677321c721fd Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Dec 2015 13:16:44 +0100 Subject: [PATCH 21/65] KEYCLOAK-2240 Internationalize admin console title --- .../src/main/resources/theme/base/admin/index.ftl | 2 +- .../theme/base/admin/messages/admin-messages_en.properties | 2 ++ .../src/main/resources/theme/base/admin/resources/js/app.js | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl index f6bbfa5c2f..243c79b613 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl +++ b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl @@ -1,7 +1,7 @@ - Keycloak Admin Console + <#if properties.styles?has_content> diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index ca128ccaa8..1fc4e75ed0 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -1,3 +1,5 @@ +consoleTitle=Keycloak Admin Console + # Common messages enabled=Enabled name=Name diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index cebb3d4cf9..5a2c7470ee 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -61,7 +61,11 @@ angular.element(document).ready(function () { module.factory('Auth', function() { return auth; }); - angular.bootstrap(document, ["keycloak"]); + var injector = angular.bootstrap(document, ["keycloak"]); + + injector.get('$translate')('consoleTitle').then(function(consoleTitle) { + document.title=consoleTitle; + }); }, function() { window.location.reload(); }); From 9a921f66ffe1e692b3d70c90d67d16351d904d47 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Dec 2015 15:35:23 +0100 Subject: [PATCH 22/65] KEYCLOAK-2043 .well-known/openid-configuration doesn't set cache-control header --- .../org/keycloak/protocol/oidc/OIDCLoginProtocolService.java | 2 ++ 1 file changed, 2 insertions(+) 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 3d7e93c359..9ad9c9558f 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -1,6 +1,7 @@ package org.keycloak.protocol.oidc; import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.OAuth2Constants; import org.keycloak.events.EventBuilder; @@ -190,6 +191,7 @@ public class OIDCLoginProtocolService { @GET @Path("certs") @Produces(MediaType.APPLICATION_JSON) + @NoCache public JSONWebKeySet certs() { JSONWebKeySet keySet = new JSONWebKeySet(); keySet.setKeys(new JWK[]{JWKBuilder.create().rs256(realm.getPublicKey())}); From b90409c5e4adcd32771e1097896e2d94cfa97ce4 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Mon, 21 Dec 2015 16:36:13 -0500 Subject: [PATCH 23/65] refactor client create --- .../theme/base/admin/resources/js/app.js | 4 +- .../admin/resources/js/controllers/clients.js | 154 +++++++++++++----- .../resources/partials/client-detail.html | 31 +--- .../resources/partials/create-client.html | 83 ++++++++++ .../keycloak/protocol/saml/SamlClient.java | 127 +++++++++++++++ .../saml/SamlClientRepresentation.java | 60 +++++++ .../protocol/saml/SamlProtocolFactory.java | 48 ++++++ .../protocol/LoginProtocolFactory.java | 10 ++ .../oidc/OIDCLoginProtocolFactory.java | 47 ++++++ .../AbstractClientRegistrationProvider.java | 2 +- .../services/managers/ClientManager.java | 19 ++- .../testsuite/rule/AbstractKeycloakRule.java | 1 + 12 files changed, 519 insertions(+), 67 deletions(-) create mode 100755 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 5a2c7470ee..e5e1bb919b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1132,7 +1132,7 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'UserRoleMappingCtrl' }) .when('/create/client/:realm', { - templateUrl : resourceUrl + '/partials/client-detail.html', + templateUrl : resourceUrl + '/partials/create-client.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); @@ -1150,7 +1150,7 @@ module.config([ '$routeProvider', function($routeProvider) { return ServerInfoLoader(); } }, - controller : 'ClientDetailCtrl' + controller : 'CreateClientCtrl' }) .when('/realms/:realm/clients/:client', { templateUrl : resourceUrl + '/partials/client-detail.html', diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index ec28ef45b0..14243077af 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -736,7 +736,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, "bearer-only" ]; - $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + $scope.protocols = ['openid-connect', + 'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort(); $scope.templates = [ {name:'NONE'}]; for (var i = 0; i < templates.length; i++) { @@ -765,7 +766,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, ]; $scope.realm = realm; - $scope.create = !client.clientId; $scope.samlAuthnStatement = false; $scope.samlMultiValuedRoles = false; $scope.samlServerSignature = false; @@ -870,20 +870,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, if (!$scope.create) { $scope.client = angular.copy(client); updateProperties(); - } else { - $scope.client = { - enabled: true, - standardFlowEnabled: true, - attributes: {} - }; - $scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value; - $scope.client.redirectUris = []; - $scope.accessType = $scope.accessTypes[0]; - $scope.protocol = $scope.protocols[0]; - $scope.signatureAlgorithm = $scope.signatureAlgorithms[1]; - $scope.nameIdFormat = $scope.nameIdFormats[0]; - $scope.samlAuthnStatement = true; - $scope.samlForceNameIdFormat = false; } @@ -1055,28 +1041,15 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, if ($scope.client.protocol != 'saml' && !$scope.client.bearerOnly && ($scope.client.standardFlowEnabled || $scope.client.implicitFlowEnabled) && (!$scope.client.redirectUris || $scope.client.redirectUris.length == 0)) { Notifications.error("You must specify at least one redirect uri"); } else { - if ($scope.create) { - Client.save({ - realm: realm.realm, - client: '' - }, $scope.client, function (data, headers) { - $scope.changed = false; - var l = headers().location; - var id = l.substring(l.lastIndexOf("/") + 1); - $location.url("/realms/" + realm.realm + "/clients/" + id); - Notifications.success("The client has been created."); - }); - } else { - Client.update({ - realm : realm.realm, - client : client.id - }, $scope.client, function() { - $scope.changed = false; - client = angular.copy($scope.client); - $location.url("/realms/" + realm.realm + "/clients/" + client.id); - Notifications.success("Your changes have been saved to the client."); - }); - } + Client.update({ + realm : realm.realm, + client : client.id + }, $scope.client, function() { + $scope.changed = false; + client = angular.copy($scope.client); + $location.url("/realms/" + realm.realm + "/clients/" + client.id); + Notifications.success("Your changes have been saved to the client."); + }); } }; @@ -1089,6 +1062,111 @@ 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 = ['openid-connect', + 'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + + $scope.templates = [ {name:'NONE'}]; + for (var i = 0; i < templates.length; i++) { + var template = templates[i]; + $scope.templates.push(template); + } + + $scope.realm = realm; + + $scope.client = { + enabled: true, + attributes: {} + }; + $scope.client.redirectUris = []; + $scope.protocol = $scope.protocols[0]; + + + $scope.importFile = function(fileContent){ + console.debug(fileContent); + ClientDescriptionConverter.save({ + realm: realm.realm + }, fileContent, function (data) { + $scope.client = data; + $scope.importing = true; + }); + }; + + $scope.viewImportDetails = function() { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-object.html', + controller: 'ObjectModalCtrl', + resolve: { + object: function () { + return $scope.client; + } + } + }) + }; + + $scope.switchChange = function() { + $scope.changed = true; + } + + $scope.changeProtocol = function() { + if ($scope.protocol == "openid-connect") { + $scope.client.protocol = "openid-connect"; + } else if ($scope.protocol == "saml") { + $scope.client.protocol = "saml"; + } + }; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + function isChanged() { + if (!angular.equals($scope.client, client)) { + return true; + } + return false; + } + + $scope.$watch('client', function() { + $scope.changed = isChanged(); + }, true); + + + $scope.save = function() { + + $scope.client.protocol = $scope.protocol; + + if ($scope.client.protocol == 'openid-connect' && !$scope.client.rootUrl) { + Notifications.error("You must specify the root URL of application"); + } + + if ($scope.client.protocol == 'saml' && !$scope.client.adminUrl) { + Notifications.error("You must specify the SAML Endpoint URL"); + } + + Client.save({ + realm: realm.realm, + client: '' + }, $scope.client, function (data, headers) { + $scope.changed = false; + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + $location.url("/realms/" + realm.realm + "/clients/" + id); + Notifications.success("The client has been created."); + }); + }; + + $scope.reset = function() { + $route.reload(); + }; + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/clients"); + }; +}); + module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, templates, Notifications, Client, ClientTemplate, ClientRealmScopeMapping, ClientClientScopeMapping, ClientRole, diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 6a2bd481df..f697c048e8 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -2,30 +2,15 @@
    -
    - - -
    - - -
    - -
    - - -
    -
    -
    - +
    @@ -250,14 +235,14 @@ {{:: 'valid-redirect-uris.tooltip' | translate}}
    -
    +
    {{:: 'base-url.tooltip' | translate}}
    -
    +
    {{:: 'idp-sso-relay-state.tooltip' | translate}}
    -
    +
    @@ -342,11 +327,7 @@
    -
    - - -
    -
    +
    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html new file mode 100755 index 0000000000..dd2a9626b6 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html @@ -0,0 +1,83 @@ +
    + + + + + + +
    +
    + + +
    + + +
    + +
    + + +
    +
    + +
    + +
    + +
    + {{:: 'client-id.tooltip' | translate}} +
    +
    + +
    +
    + +
    +
    + {{:: 'client-protocol.tooltip' | translate}} +
    +
    + +
    +
    + +
    +
    + Client template this client inherits configuration from +
    +
    + +
    + +
    + {{:: 'root-url.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'master-saml-processing-url.tooltip' | translate}} +
    +
    +
    +
    + + +
    +
    + +
    + + \ No newline at end of file diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java new file mode 100755 index 0000000000..241cc268f8 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java @@ -0,0 +1,127 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.models.ClientModel; +import org.keycloak.saml.SignatureAlgorithm; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlClient { + public static final String SAML_SIGNING_PRIVATE_KEY = "saml.signing.private.key"; + protected ClientModel client; + + public SamlClient(ClientModel client) { + this.client = client; + } + + public String getCanonicalizationMethod() { + return client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + } + + public void setCanonicalizationMethod(String value) { + client.setAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE, value); + } + + public SignatureAlgorithm getSignatureAlgorithm() { + String alg = client.getAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM); + if (alg != null) { + SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); + if (algorithm != null) + return algorithm; + } + return SignatureAlgorithm.RSA_SHA256; + } + + public void setSignatureAlgorithm(SignatureAlgorithm algorithm) { + client.setAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM, algorithm.name()); + } + + public String getNameIDFormat() { + return client.getAttributes().get(SamlProtocol.SAML_NAME_ID_FORMAT_ATTRIBUTE); + } + public void setNameIDFormat(String format) { + client.setAttribute(SamlProtocol.SAML_NAME_ID_FORMAT_ATTRIBUTE, format); + } + + public boolean includeAuthnStatement() { + return "true".equals(client.getAttribute(SamlProtocol.SAML_AUTHNSTATEMENT)); + } + + public void setIncludeAuthnStatement(boolean val) { + client.setAttribute(SamlProtocol.SAML_AUTHNSTATEMENT, Boolean.toString(val)); + } + + public boolean forceNameIDFormat() { + return "true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); + + } + public void setForceNameIDFormat(boolean val) { + client.setAttribute(SamlProtocol.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val)); + } + + public boolean requiresRealmSignature(ClientModel client) { + return "true".equals(client.getAttribute(SamlProtocol.SAML_SERVER_SIGNATURE)); + } + + public void setRequiresRealmSignature(boolean val) { + client.setAttribute(SamlProtocol.SAML_SERVER_SIGNATURE, Boolean.toString(val)); + + } + + public boolean forcePostBinding(ClientModel client) { + return "true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING)); + } + + public void setForcePostBinding(boolean val) { + client.setAttribute(SamlProtocol.SAML_FORCE_POST_BINDING, Boolean.toString(val)); + + } + public boolean samlAssertionSignature(ClientModel client) { + return "true".equals(client.getAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE)); + } + + public void setAssertionSignature(boolean val) { + client.setAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE , Boolean.toString(val)); + + } + public boolean requiresEncryption(ClientModel client) { + return "true".equals(client.getAttribute(SamlProtocol.SAML_ENCRYPT)); + } + + + public void setRequiresEncryption(boolean val) { + client.setAttribute(SamlProtocol.SAML_ENCRYPT, Boolean.toString(val)); + + } + + public boolean requiresClientSignature(ClientModel client) { + return "true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE)); + } + + public void setRequiresClientSignature(boolean val) { + client.setAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE , Boolean.toString(val)); + + } + + public String getClientSigningCertificate() { + return client.getAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE); + } + + public void setClientSigningCertificate(String val) { + client.setAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, val); + + } + + public String getClientSigningPrivateKey() { + return client.getAttribute(SAML_SIGNING_PRIVATE_KEY); + } + + public void setClientSigningPrivateKey(String val) { + client.setAttribute(SAML_SIGNING_PRIVATE_KEY, val); + + } + + + +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java new file mode 100755 index 0000000000..4151d625a7 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java @@ -0,0 +1,60 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.models.ClientModel; +import org.keycloak.representations.idm.ClientRepresentation; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlClientRepresentation { + protected ClientRepresentation rep; + + public SamlClientRepresentation(ClientRepresentation rep) { + this.rep = rep; + } + + public String getCanonicalizationMethod() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + } + + public String getSignatureAlgorithm() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_SIGNATURE_ALGORITHM); + } + + public String getNameIDFormat() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_NAME_ID_FORMAT_ATTRIBUTE); + + } + + public String getIncludeAuthnStatement() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_AUTHNSTATEMENT); + + } + + public String getForceNameIDFormat() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE); + } + + public String getSamlServerSignature() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_SERVER_SIGNATURE); + + } + + public String getForcePostBinding() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_FORCE_POST_BINDING); + + } + public String getClientSignature() { + if (rep.getAttributes() == null) return null; + return rep.getAttributes().get(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE); + + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java index 7dcc86695b..f7b4d1e97b 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java @@ -6,15 +6,20 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.AbstractLoginProtocolFactory; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper; +import org.keycloak.representations.idm.CertificateRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants; +import javax.xml.crypto.dsig.CanonicalizationMethod; import java.util.ArrayList; import java.util.List; @@ -95,4 +100,47 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { client.addProtocolMapper(model); } } + + @Override + public void setupClientDefaults(ClientRepresentation clientRep, ClientModel newClient) { + SamlClientRepresentation rep = new SamlClientRepresentation(clientRep); + SamlClient client = new SamlClient(newClient); + if (clientRep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true); + if (rep.getCanonicalizationMethod() == null) { + client.setCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE); + } + if (rep.getSignatureAlgorithm() == null) { + client.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256); + } + + if (rep.getNameIDFormat() == null) { + client.setNameIDFormat("username"); + } + + if (rep.getIncludeAuthnStatement() == null) { + client.setIncludeAuthnStatement(true); + } + + if (rep.getForceNameIDFormat() == null) { + client.setForceNameIDFormat(false); + } + + if (rep.getSamlServerSignature() == null) { + client.setRequiresRealmSignature(true); + } + if (rep.getForcePostBinding() == null) { + client.setForcePostBinding(true); + } + + if (rep.getClientSignature() == null) { + client.setRequiresClientSignature(true); + CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(newClient.getClientId()); + client.setClientSigningCertificate(info.getCertificate()); + client.setClientSigningPrivateKey(info.getPrivateKey()); + } + + if (clientRep.isFrontchannelLogout() == null) { + newClient.setFrontchannelLogout(true); + } + } } diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java index 5742381cc3..a876b194b5 100755 --- a/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java @@ -1,9 +1,11 @@ package org.keycloak.protocol; import org.keycloak.events.EventBuilder; +import org.keycloak.models.ClientModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.managers.AuthenticationManager; import java.util.List; @@ -27,4 +29,12 @@ public interface LoginProtocolFactory extends ProviderFactory { List getDefaultBuiltinMappers(); Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager); + + /** + * Setup default values for new clients. + * + * @param rep + * @param newClient + */ + void setupClientDefaults(ClientRepresentation rep, ClientModel newClient); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index b9211ad79f..8cd0d8fed5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -16,7 +16,9 @@ */ package org.keycloak.protocol.oidc; +import org.jboss.logging.Logger; import org.keycloak.common.constants.KerberosConstants; +import org.keycloak.common.util.UriUtils; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -29,12 +31,16 @@ import org.keycloak.protocol.oidc.mappers.FullNameMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.managers.AuthenticationManager; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; + import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; /** @@ -42,6 +48,7 @@ import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; * @version $Revision: 1 $ */ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { + private static Logger logger = Logger.getLogger(OIDCLoginProtocolFactory.class); public static final String USERNAME = "username"; public static final String EMAIL = "email"; @@ -159,4 +166,44 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { public String getId() { return "openid-connect"; } + + @Override + public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) { + if (rep.getRootUrl() != null && (rep.getRedirectUris() == null || rep.getRedirectUris().isEmpty())) { + String root = rep.getRootUrl(); + if (root.endsWith("/")) root = root + "*"; + else root = root + "/*"; + newClient.addRedirectUri(root); + + Set origins = new HashSet(); + String origin = UriUtils.getOrigin(root); + logger.debugv("adding default client origin: {0}" , origin); + origins.add(origin); + newClient.setWebOrigins(origins); + } + if (rep.isBearerOnly() == null + && rep.isPublicClient() == null) { + newClient.setPublicClient(true); + } + if (rep.isBearerOnly() == null) newClient.setBearerOnly(false); + if (rep.getAdminUrl() == null && rep.getRootUrl() != null) { + newClient.setManagementUrl(rep.getRootUrl()); + } + + + // Backwards compatibility only + if (rep.isDirectGrantsOnly() != null) { + logger.warn("Using deprecated 'directGrantsOnly' configuration in JSON representation. It will be removed in future versions"); + newClient.setStandardFlowEnabled(!rep.isDirectGrantsOnly()); + newClient.setDirectAccessGrantsEnabled(rep.isDirectGrantsOnly()); + } else { + if (rep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true); + if (rep.isDirectAccessGrantsEnabled() == null) newClient.setDirectAccessGrantsEnabled(true); + + } + + if (rep.isImplicitFlowEnabled() == null) newClient.setImplicitFlowEnabled(false); + if (rep.isPublicClient() == null) newClient.setPublicClient(true); + if (rep.isFrontchannelLogout() == null) newClient.setFrontchannelLogout(false); + } } 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 0666fab32c..d011089113 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java @@ -34,7 +34,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist auth.requireCreate(); try { - ClientModel clientModel = ClientManager.createClient(session, session.getContext().getRealm(), client, true); + ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true); if (client.getClientId() == null) { clientModel.setClientId(clientModel.getId()); } diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index 358860f027..e823ac29f3 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -15,6 +15,8 @@ import org.keycloak.models.UserSessionProvider; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.adapters.config.BaseRealmConfig; @@ -45,10 +47,25 @@ public class ClientManager { public ClientManager() { } + /** + * Should not be called from an import. This really expects that the client is created from the admin console. + * + * @param session + * @param realm + * @param rep + * @param addDefaultRoles + * @return + */ public static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation rep, boolean addDefaultRoles) { ClientModel client = RepresentationToModel.createClient(session, realm, rep, addDefaultRoles); - // remove default mappers + if (rep.getProtocol() != null) { + LoginProtocolFactory providerFactory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, rep.getProtocol()); + providerFactory.setupClientDefaults(rep, client); + } + + + // remove default mappers if there is a template if (rep.getProtocolMappers() == null && rep.getClientTemplate() != null) { Set mappers = client.getProtocolMappers(); for (ProtocolMapperModel mapper : mappers) client.removeProtocolMapper(mapper); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java index 155bdf117f..f4ef63272a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java @@ -271,6 +271,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource { } }, 10, 500); + Thread.sleep(100); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } From 1747e0981fe72ba276d28a3bfcd332f7e370a7cc Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 16 Dec 2015 16:41:41 +0100 Subject: [PATCH 24/65] KEYCLOAK-2154 Added Group mapper for LDAP. LDAP mappers improvements and fixes --- ...serFederationMapperTypeRepresentation.java | 13 +- .../BasePropertiesFederationProvider.java | 5 + .../kerberos/KerberosFederationProvider.java | 5 + .../ldap/LDAPFederationProvider.java | 50 ++ .../keycloak/federation/ldap/LDAPUtils.java | 110 +++ .../federation/ldap/idm/model/LDAPDn.java | 14 + .../mappers/AbstractLDAPFederationMapper.java | 67 +- .../AbstractLDAPFederationMapperFactory.java | 17 + .../mappers/FullNameLDAPFederationMapper.java | 28 +- .../FullNameLDAPFederationMapperFactory.java | 27 +- .../mappers/LDAPFederationMapperBridge.java | 76 +++ .../mappers/RoleLDAPFederationMapper.java | 585 ---------------- .../UserAttributeLDAPFederationMapper.java | 19 +- ...rAttributeLDAPFederationMapperFactory.java | 31 +- .../mappers/UserRolesRetrieveStrategy.java | 124 ---- .../membership/CommonLDAPGroupMapper.java | 15 + .../CommonLDAPGroupMapperConfig.java | 70 ++ .../membership/LDAPGroupMapperMode.java | 29 + .../mappers/membership/MembershipType.java | 17 + .../membership/UserRolesRetrieveStrategy.java | 111 +++ .../group/GroupLDAPFederationMapper.java | 634 ++++++++++++++++++ .../GroupLDAPFederationMapperFactory.java | 183 +++++ .../membership/group/GroupMapperConfig.java | 108 +++ .../membership/group/GroupTreeResolver.java | 187 ++++++ .../role/RoleLDAPFederationMapper.java | 432 ++++++++++++ .../RoleLDAPFederationMapperFactory.java | 102 ++- .../membership/role/RoleMapperConfig.java | 97 +++ ...ycloak.mappers.UserFederationMapperFactory | 3 +- .../ldap/idm/model/GroupTreeResolverTest.java | 108 +++ .../federation/ldap/idm/model/LDAPDnTest.java | 1 + .../admin/resources/js/controllers/users.js | 12 +- .../mappers/UserFederationMapper.java | 9 + .../mappers/UserFederationMapperFactory.java | 10 + .../models/UserFederationManager.java | 38 +- .../models/UserFederationProvider.java | 12 + .../admin/UserFederationProviderResource.java | 2 + .../DummyUserFederationProvider.java | 5 + .../federation/FederationTestUtils.java | 119 +++- .../LDAPGroupMapper2WaySyncTest.java | 238 +++++++ .../federation/LDAPGroupMapperSyncTest.java | 224 +++++++ .../federation/LDAPGroupMapperTest.java | 291 ++++++++ .../LDAPMultipleAttributesTest.java | 3 +- .../federation/LDAPRoleMappingsTest.java | 46 +- .../federation/SyncProvidersTest.java | 13 +- .../src/test/resources/ldap/users.ldif | 5 + 45 files changed, 3444 insertions(+), 851 deletions(-) create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java delete mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java delete mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapper.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/LDAPGroupMapperMode.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/UserRolesRetrieveStrategy.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupMapperConfig.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupTreeResolver.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapper.java rename federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/{ => membership/role}/RoleLDAPFederationMapperFactory.java (60%) create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleMapperConfig.java create mode 100644 federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/GroupTreeResolverTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapper2WaySyncTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java diff --git a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java index 48a0054f9f..386a15c801 100644 --- a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java @@ -1,7 +1,9 @@ package org.keycloak.representations.idm; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; /** * @author Marek Posolda @@ -13,8 +15,9 @@ public class UserFederationMapperTypeRepresentation { protected String helpText; protected UserFederationMapperSyncConfigRepresentation syncConfig; - protected List properties = new LinkedList<>(); + protected Map defaultConfig = new HashMap<>(); + public String getId() { return id; @@ -63,4 +66,12 @@ public class UserFederationMapperTypeRepresentation { public void setProperties(List properties) { this.properties = properties; } + + public Map getDefaultConfig() { + return defaultConfig; + } + + public void setDefaultConfig(Map defaultConfig) { + this.defaultConfig = defaultConfig; + } } diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java index d6475aaa61..eb4b9bb1fc 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java @@ -96,6 +96,11 @@ public abstract class BasePropertiesFederationProvider implements UserFederation return Collections.emptyList(); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return Collections.emptyList(); + } + @Override public void preRemove(RealmModel realm) { // complete We don't care about the realm being removed diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java index 65831f250c..e45debdc6f 100755 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -96,6 +96,11 @@ public class KerberosFederationProvider implements UserFederationProvider { return Collections.emptyList(); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return Collections.emptyList(); + } + @Override public void preRemove(RealmModel realm) { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index f9f6600390..714a97a16b 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -3,6 +3,7 @@ package org.keycloak.federation.ldap; import org.jboss.logging.Logger; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; +import org.keycloak.federation.ldap.idm.model.LDAPDn; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; @@ -10,6 +11,8 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilde import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; @@ -29,6 +32,8 @@ import org.keycloak.common.constants.KerberosConstants; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -185,6 +190,51 @@ public class LDAPFederationProvider implements UserFederationProvider { return searchResults; } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + Set federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId()); + for (UserFederationMapperModel mapperModel : federationMappers) { + LDAPFederationMapper ldapMapper = getMapper(mapperModel); + List users = ldapMapper.getGroupMembers(mapperModel, this, realm, group, firstResult, maxResults); + + // Sufficient for now + if (users.size() > 0) { + return users; + } + } + + return Collections.emptyList(); + } + + public List loadUsersByLDAPDns(Collection userDns, RealmModel realm) { + // We have dns of users, who are members of our group. Load them now + LDAPQuery query = LDAPUtils.createQueryForUserSearch(this, realm); + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition[] orSubconditions = new Condition[userDns.size()]; + int index = 0; + for (LDAPDn userDn : userDns) { + Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue()); + orSubconditions[index] = condition; + index++; + } + Condition orCondition = conditionsBuilder.orCondition(orSubconditions); + query.addWhereCondition(orCondition); + List ldapUsers = query.getResultList(); + + // We have ldapUsers, Need to load users from KC DB or import them here + List result = new LinkedList<>(); + for (LDAPObject ldapUser : ldapUsers) { + String username = LDAPUtils.getUsername(ldapUser, getLdapIdentityStore().getConfig()); + UserModel kcUser = session.users().getUserByUsername(username, realm); + if (!model.getId().equals(kcUser.getFederationLink())) { + logger.warnf("Incorrect federation provider of user %s" + kcUser.getUsername()); + } else { + result.add(kcUser); + } + } + return result; + } + protected List searchLDAP(RealmModel realm, Map attributes, int maxResults) { List results = new ArrayList(); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java index 7594ed45bb..70488ad27f 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java @@ -1,5 +1,8 @@ package org.keycloak.federation.ldap; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.keycloak.federation.ldap.idm.model.LDAPDn; @@ -9,6 +12,8 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.MembershipType; +import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; @@ -97,4 +102,109 @@ public class LDAPUtils { "Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn()); } } + + + // roles & groups + + public static LDAPObject createLDAPGroup(LDAPFederationProvider ldapProvider, String groupName, String groupNameAttribute, Collection objectClasses, + String parentDn, Map> additionalAttributes) { + LDAPObject ldapObject = new LDAPObject(); + + ldapObject.setRdnAttributeName(groupNameAttribute); + ldapObject.setObjectClasses(objectClasses); + ldapObject.setSingleAttribute(groupNameAttribute, groupName); + + LDAPDn roleDn = LDAPDn.fromString(parentDn); + roleDn.addFirst(groupNameAttribute, groupName); + ldapObject.setDn(roleDn); + + for (Map.Entry> attrEntry : additionalAttributes.entrySet()) { + ldapObject.setAttribute(attrEntry.getKey(), attrEntry.getValue()); + } + + ldapProvider.getLdapIdentityStore().add(ldapObject); + return ldapObject; + } + + /** + * Add ldapChild as member of ldapParent and save ldapParent to LDAP. + * + * @param ldapProvider + * @param membershipType how is 'member' attribute saved (full DN or just uid) + * @param memberAttrName usually 'member' + * @param ldapParent role or group + * @param ldapChild usually user (or child group or child role) + * @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it + */ + public static void addMember(LDAPFederationProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) { + + Set memberships = getExistingMemberships(memberAttrName, ldapParent); + + // Remove membership placeholder if present + if (membershipType == MembershipType.DN) { + for (String membership : memberships) { + if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) { + memberships.remove(membership); + break; + } + } + } + + String membership = getMemberValueOfChildObject(ldapChild, membershipType); + + memberships.add(membership); + ldapParent.setAttribute(memberAttrName, memberships); + + if (sendLDAPUpdateRequest) { + ldapProvider.getLdapIdentityStore().update(ldapParent); + } + } + + /** + * Remove ldapChild as member of ldapParent and save ldapParent to LDAP. + * + * @param ldapProvider + * @param membershipType how is 'member' attribute saved (full DN or just uid) + * @param memberAttrName usually 'member' + * @param ldapParent role or group + * @param ldapChild usually user (or child group or child role) + * @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it + */ + public static void deleteMember(LDAPFederationProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) { + Set memberships = getExistingMemberships(memberAttrName, ldapParent); + + String userMembership = getMemberValueOfChildObject(ldapChild, membershipType); + + memberships.remove(userMembership); + + // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here) + if (memberships.size() == 0 && membershipType== MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) { + memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE); + } + + ldapParent.setAttribute(memberAttrName, memberships); + ldapProvider.getLdapIdentityStore().update(ldapParent); + } + + /** + * Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup + * + * @param memberAttrName usually 'member' + * @param ldapRole + * @return + */ + public static Set getExistingMemberships(String memberAttrName, LDAPObject ldapRole) { + Set memberships = ldapRole.getAttributeAsSet(memberAttrName); + if (memberships == null) { + memberships = new HashSet<>(); + } + return memberships; + } + + /** + * Get value to be used as attribute 'member' in some parent ldapObject + */ + public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) { + return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName()); + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java index be5e6b9d94..07f15d2a05 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java @@ -25,6 +25,20 @@ public class LDAPDn { return dn; } + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LDAPDn)) { + return false; + } + + return toString().equals(obj.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + @Override public String toString() { return toString(entries); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java index fd05cf87b2..9b08f5fe3b 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java @@ -1,32 +1,77 @@ package org.keycloak.federation.ldap.mappers; +import java.util.Collections; +import java.util.List; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.UserModel; +import org.keycloak.mappers.UserFederationMapper; /** + * Stateful per-request object + * * @author Marek Posolda */ -public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapper { +public abstract class AbstractLDAPFederationMapper { - @Override - public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { - throw new IllegalStateException("Not supported"); + protected final UserFederationMapperModel mapperModel; + protected final LDAPFederationProvider ldapProvider; + protected final RealmModel realm; + + public AbstractLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { + this.mapperModel = mapperModel; + this.ldapProvider = ldapProvider; + this.realm = realm; } - @Override - public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { - throw new IllegalStateException("Not supported"); + /** + * @see UserFederationMapper#syncDataFromFederationProviderToKeycloak(UserFederationMapperModel, UserFederationProvider, KeycloakSession, RealmModel) + */ + public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() { + return new UserFederationSyncResult(); } - @Override - public void close() { - + /** + * @see UserFederationMapper#syncDataFromKeycloakToFederationProvider(UserFederationMapperModel, UserFederationProvider, KeycloakSession, RealmModel) + */ + public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() { + return new UserFederationSyncResult(); } - protected boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { + /** + * @see LDAPFederationMapper#beforeLDAPQuery(UserFederationMapperModel, LDAPQuery) + */ + public abstract void beforeLDAPQuery(LDAPQuery query); + + /** + * @see LDAPFederationMapper#proxy(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel) + */ + public abstract UserModel proxy(LDAPObject ldapUser, UserModel delegate); + + /** + * @see LDAPFederationMapper#onRegisterUserToLDAP(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel) + */ + public abstract void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser); + + /** + * @see LDAPFederationMapper#onImportUserFromLDAP(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel, boolean) + */ + public abstract void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate); + + public List getGroupMembers(GroupModel group, int firstResult, int maxResults) { + return Collections.emptyList(); + } + + + public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { String paramm = mapperModel.getConfig().get(paramName); return Boolean.parseBoolean(paramm); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java index 8dfce50c16..5ab13f1b15 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java @@ -4,11 +4,16 @@ import java.util.List; import java.util.Map; import org.keycloak.Config; +import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.mappers.MapperConfigValidationException; +import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapperFactory; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; @@ -23,10 +28,22 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat // Used to map roles from LDAP to UserModel users public static final String ROLE_MAPPER_CATEGORY = "Role Mapper"; + + // Used to map group from LDAP to UserModel users + public static final String GROUP_MAPPER_CATEGORY = "Group Mapper"; + @Override public void init(Config.Scope config) { } + @Override + public UserFederationMapper create(KeycloakSession session) { + return new LDAPFederationMapperBridge(this); + } + + // Used just by LDAPFederationMapperBridge. + protected abstract AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm); + @Override public String getFederationProviderType() { return LDAPFederationProviderFactory.PROVIDER_NAME; diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java index 03235f6b98..2069d25589 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java @@ -24,9 +24,13 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute"; public static final String READ_ONLY = "read.only"; + public FullNameLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { + super(mapperModel, ldapProvider, realm); + } + @Override - public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { - String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) { + String ldapFullNameAttrName = getLdapFullNameAttrName(); String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName); if (fullName == null) { return; @@ -45,19 +49,19 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { } @Override - public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { - String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) { + String ldapFullNameAttrName = getLdapFullNameAttrName(); String fullName = getFullName(localUser.getFirstName(), localUser.getLastName()); ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName); - if (isReadOnly(mapperModel)) { + if (isReadOnly()) { ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName); } } @Override - public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) { - if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) { + public UserModel proxy(LDAPObject ldapUser, UserModel delegate) { + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly()) { TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) { @@ -82,7 +86,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { ensureTransactionStarted(); - String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + String ldapFullNameAttrName = getLdapFullNameAttrName(); ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName); } @@ -95,8 +99,8 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { } @Override - public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { - String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + public void beforeLDAPQuery(LDAPQuery query) { + String ldapFullNameAttrName = getLdapFullNameAttrName(); query.addReturningLdapAttribute(ldapFullNameAttrName); // Change conditions and compute condition for fullName from the conditions for firstName and lastName. Right now just "equal" condition is supported @@ -137,7 +141,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { query.addWhereCondition(fullNameCondition); } - protected String getLdapFullNameAttrName(UserFederationMapperModel mapperModel) { + protected String getLdapFullNameAttrName() { String ldapFullNameAttrName = mapperModel.getConfig().get(LDAP_FULL_NAME_ATTRIBUTE); return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName; } @@ -154,7 +158,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { } } - private boolean isReadOnly(UserFederationMapperModel mapperModel) { + private boolean isReadOnly() { return parseBooleanParameter(mapperModel, READ_ONLY); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java index e26cd66ecf..e08429ef23 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java @@ -1,14 +1,20 @@ package org.keycloak.federation.ldap.mappers; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.keycloak.federation.ldap.LDAPConfig; +import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.provider.ProviderConfigProperty; /** @@ -22,11 +28,11 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM static { ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", - "Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN); + "Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(userModelAttribute); ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only", - "For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + "For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null); configProperties.add(readOnly); } @@ -50,6 +56,19 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM return configProperties; } + @Override + public Map getDefaultConfig(UserFederationProviderModel providerModel) { + Map defaultValues = new HashMap<>(); + LDAPConfig config = new LDAPConfig(providerModel.getConfig()); + + defaultValues.put(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN); + + String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true"; + defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); + + return defaultValues; + } + @Override public String getId() { return PROVIDER_ID; @@ -61,7 +80,7 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM } @Override - public UserFederationMapper create(KeycloakSession session) { - return new FullNameLDAPFederationMapper(); + protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) { + return new FullNameLDAPFederationMapper(mapperModel, federationProvider, realm); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java new file mode 100644 index 0000000000..b77bf3836f --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java @@ -0,0 +1,76 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.List; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.UserModel; + +/** + * Sufficient if mapper implementation is stateless and doesn't need to "close" any state + * + * @author Marek Posolda + */ +public class LDAPFederationMapperBridge implements LDAPFederationMapper { + + private final AbstractLDAPFederationMapperFactory factory; + + public LDAPFederationMapperBridge(AbstractLDAPFederationMapperFactory factory) { + this.factory = factory; + } + + // Sync groups from LDAP to Keycloak DB + @Override + public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { + return getDelegate(mapperModel, federationProvider, realm).syncDataFromFederationProviderToKeycloak(); + } + + @Override + public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { + return getDelegate(mapperModel, federationProvider, realm).syncDataFromKeycloakToFederationProvider(); + } + + @Override + public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { + getDelegate(mapperModel, ldapProvider, realm).onImportUserFromLDAP(ldapUser, user, isCreate); + } + + @Override + public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { + getDelegate(mapperModel, ldapProvider, realm).onRegisterUserToLDAP(ldapUser, localUser); + } + + @Override + public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) { + return getDelegate(mapperModel, ldapProvider, realm).proxy(ldapUser, delegate); + } + + @Override + public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + // Improve if needed + getDelegate(mapperModel, null, null).beforeLDAPQuery(query); + } + + + @Override + public List getGroupMembers(UserFederationMapperModel mapperModel, UserFederationProvider ldapProvider, RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults); + } + + private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) { + LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; + return factory.createMapper(mapperModel, ldapProvider, realm); + } + + @Override + public void close() { + + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java deleted file mode 100644 index 0960bb935e..0000000000 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ /dev/null @@ -1,585 +0,0 @@ -package org.keycloak.federation.ldap.mappers; - -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import org.jboss.logging.Logger; -import org.keycloak.federation.ldap.LDAPFederationProvider; -import org.keycloak.federation.ldap.idm.model.LDAPDn; -import org.keycloak.federation.ldap.idm.model.LDAPObject; -import org.keycloak.federation.ldap.idm.query.Condition; -import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; -import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.LDAPConstants; -import org.keycloak.models.ModelException; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleContainerModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserFederationMapperModel; -import org.keycloak.models.UserFederationProvider; -import org.keycloak.models.UserFederationSyncResult; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.UserModelDelegate; - -/** - * Map realm roles or roles of particular client to LDAP groups - * - * @author Marek Posolda - */ -public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { - - private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class); - - // LDAP DN where are roles of this tree saved. - public static final String ROLES_DN = "roles.dn"; - - // Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn" - public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute"; - - // Object classes of the role object. - public static final String ROLE_OBJECT_CLASSES = "role.object.classes"; - - // Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member" - public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute"; - - // See docs for MembershipType enum - public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type"; - - // Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID) - public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping"; - - // ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false - public static final String CLIENT_ID = "client.id"; - - // See docs for Mode enum - public static final String MODE = "mode"; - - // See docs for UserRolesRetriever enum - public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy"; - - // Customized LDAP filter which is added to the whole LDAP query - public static final String ROLES_LDAP_FILTER = "roles.ldap.filter"; - - @Override - public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { - Mode mode = getMode(mapperModel); - - // For now, import LDAP role mappings just during create - if (mode == Mode.IMPORT && isCreate) { - - List ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser); - - // Import role mappings from LDAP into Keycloak DB - String roleNameAttr = getRoleNameLdapAttribute(mapperModel); - for (LDAPObject ldapRole : ldapRoles) { - String roleName = ldapRole.getAttributeAsString(roleNameAttr); - - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - RoleModel role = roleContainer.getRole(roleName); - - logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername()); - user.grantRole(role); - } - } - } - - @Override - public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { - } - - - // Sync roles from LDAP to Keycloak DB - @Override - public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { - LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; - UserFederationSyncResult syncResult = new UserFederationSyncResult() { - - @Override - public String getStatus() { - return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated()); - } - - }; - - logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); - - // Send LDAP query - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); - List ldapRoles = ldapQuery.getResultList(); - - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); - for (LDAPObject ldapRole : ldapRoles) { - String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); - - if (roleContainer.getRole(roleName) == null) { - logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName); - roleContainer.addRole(roleName); - syncResult.increaseAdded(); - } else { - syncResult.increaseUpdated(); - } - } - - return syncResult; - } - - - // Sync roles from Keycloak back to LDAP - @Override - public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) { - LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; - UserFederationSyncResult syncResult = new UserFederationSyncResult() { - - @Override - public String getStatus() { - return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated()); - } - - }; - - logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); - - // Send LDAP query to see which roles exists there - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); - List ldapRoles = ldapQuery.getResultList(); - - Set ldapRoleNames = new HashSet<>(); - String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); - for (LDAPObject ldapRole : ldapRoles) { - String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); - ldapRoleNames.add(roleName); - } - - - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - Set keycloakRoles = roleContainer.getRoles(); - - for (RoleModel keycloakRole : keycloakRoles) { - String roleName = keycloakRole.getName(); - if (ldapRoleNames.contains(roleName)) { - syncResult.increaseUpdated(); - } else { - logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName); - createLDAPRole(mapperModel, roleName, ldapProvider); - syncResult.increaseAdded(); - } - } - - return syncResult; - } - - - public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { - LDAPQuery ldapQuery = new LDAPQuery(ldapProvider); - - // For now, use same search scope, which is configured "globally" and used for user's search. - ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope()); - - String rolesDn = getRolesDn(mapperModel); - ldapQuery.setSearchDn(rolesDn); - - Collection roleObjectClasses = getRoleObjectClasses(mapperModel, ldapProvider); - ldapQuery.addObjectClasses(roleObjectClasses); - - String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); - - String customFilter = mapperModel.getConfig().get(RoleLDAPFederationMapper.ROLES_LDAP_FILTER); - if (customFilter != null && customFilter.trim().length() > 0) { - Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter); - ldapQuery.addWhereCondition(customFilterCondition); - } - - String membershipAttr = getMembershipLdapAttribute(mapperModel); - ldapQuery.addReturningLdapAttribute(rolesRdnAttr); - ldapQuery.addReturningLdapAttribute(membershipAttr); - - return ldapQuery; - } - - protected RoleContainerModel getTargetRoleContainer(UserFederationMapperModel mapperModel, RealmModel realm) { - boolean realmRolesMapping = parseBooleanParameter(mapperModel, USE_REALM_ROLES_MAPPING); - if (realmRolesMapping) { - return realm; - } else { - String clientId = mapperModel.getConfig().get(CLIENT_ID); - if (clientId == null) { - throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!"); - } - ClientModel client = realm.getClientByClientId(clientId); - if (client == null) { - throw new ModelException("Can't found requested client with clientId: " + clientId); - } - return client; - } - } - - protected String getRolesDn(UserFederationMapperModel mapperModel) { - String rolesDn = mapperModel.getConfig().get(ROLES_DN); - if (rolesDn == null) { - throw new ModelException("Roles DN is null! Check your configuration"); - } - return rolesDn; - } - - protected String getRoleNameLdapAttribute(UserFederationMapperModel mapperModel) { - String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE); - return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN; - } - - protected String getMembershipLdapAttribute(UserFederationMapperModel mapperModel) { - String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE); - return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER; - } - - protected MembershipType getMembershipTypeLdapAttribute(UserFederationMapperModel mapperModel) { - String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE); - return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; - } - - protected String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) { - return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName()); - } - - protected Collection getRoleObjectClasses(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { - String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES); - if (objectClasses == null) { - // For Active directory, the default is 'group' . For other servers 'groupOfNames' - objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES; - } - String[] objClasses = objectClasses.split(","); - - Set trimmed = new HashSet<>(); - for (String objectClass : objClasses) { - objectClass = objectClass.trim(); - if (objectClass.length() > 0) { - trimmed.add(objectClass); - } - } - return trimmed; - } - - private Mode getMode(UserFederationMapperModel mapperModel) { - String modeString = mapperModel.getConfig().get(MODE); - if (modeString == null || modeString.isEmpty()) { - throw new ModelException("Mode is missing! Check your configuration"); - } - - return Enum.valueOf(Mode.class, modeString.toUpperCase()); - } - - private UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(UserFederationMapperModel mapperModel) { - String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY); - return (strategyString!=null && !strategyString.isEmpty()) ? Enum.valueOf(UserRolesRetrieveStrategy.class, strategyString) : UserRolesRetrieveStrategy.LOAD_ROLES_BY_MEMBER_ATTRIBUTE; - } - - public LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) { - LDAPObject ldapObject = new LDAPObject(); - String roleNameAttribute = getRoleNameLdapAttribute(mapperModel); - ldapObject.setRdnAttributeName(roleNameAttribute); - ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider)); - ldapObject.setSingleAttribute(roleNameAttribute, roleName); - - LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel)); - roleDn.addFirst(roleNameAttribute, roleName); - ldapObject.setDn(roleDn); - - logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, roleDn.toString()); - ldapProvider.getLdapIdentityStore().add(ldapObject); - return ldapObject; - } - - public void addRoleMappingInLDAP(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - LDAPObject ldapRole = loadLDAPRoleByName(mapperModel, ldapProvider, roleName); - if (ldapRole == null) { - ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider); - } - - MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel); - - Set memberships = getExistingMemberships(mapperModel, ldapRole); - - // Remove membership placeholder if present - if (membershipType == MembershipType.DN) { - for (String membership : memberships) { - if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) { - memberships.remove(membership); - break; - } - } - } - - String membership = getMembershipFromUser(ldapUser, membershipType); - - memberships.add(membership); - ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships); - - ldapProvider.getLdapIdentityStore().update(ldapRole); - } - - public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) { - Set memberships = getExistingMemberships(mapperModel, ldapRole); - - MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel); - String userMembership = getMembershipFromUser(ldapUser, membershipType); - - memberships.remove(userMembership); - - // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here) - if (memberships.size() == 0 && membershipType==MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) { - memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE); - } - - ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships); - ldapProvider.getLdapIdentityStore().update(ldapRole); - } - - public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) { - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); - Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(getRoleNameLdapAttribute(mapperModel), roleName); - ldapQuery.addWhereCondition(roleNameCondition); - return ldapQuery.getFirstResult(); - } - - protected Set getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) { - String memberAttrName = getMembershipLdapAttribute(mapperModel); - Set memberships = ldapRole.getAttributeAsSet(memberAttrName); - if (memberships == null) { - memberships = new HashSet<>(); - } - return memberships; - } - - protected List getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel); - return strategy.getLDAPRoleMappings(this, mapperModel, ldapProvider, ldapUser); - } - - @Override - public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) { - final Mode mode = getMode(mapperModel); - - // For IMPORT mode, all operations are performed against local DB - if (mode == Mode.IMPORT) { - return delegate; - } else { - return new LDAPRoleMappingsUserDelegate(delegate, mapperModel, ldapProvider, ldapUser, realm, mode); - } - } - - @Override - public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { - UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel); - strategy.beforeUserLDAPQuery(mapperModel, query); - } - - - - public class LDAPRoleMappingsUserDelegate extends UserModelDelegate { - - private final UserFederationMapperModel mapperModel; - private final LDAPFederationProvider ldapProvider; - private final LDAPObject ldapUser; - private final RealmModel realm; - private final Mode mode; - - // Avoid loading role mappings from LDAP more times per-request - private Set cachedLDAPRoleMappings; - - public LDAPRoleMappingsUserDelegate(UserModel user, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, - RealmModel realm, Mode mode) { - super(user); - this.mapperModel = mapperModel; - this.ldapProvider = ldapProvider; - this.ldapUser = ldapUser; - this.realm = realm; - this.mode = mode; - } - - @Override - public Set getRealmRoleMappings() { - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - if (roleContainer.equals(realm)) { - Set ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer); - - if (mode == Mode.LDAP_ONLY) { - // Use just role mappings from LDAP - return ldapRoleMappings; - } else { - // Merge mappings from both DB and LDAP - Set modelRoleMappings = super.getRealmRoleMappings(); - ldapRoleMappings.addAll(modelRoleMappings); - return ldapRoleMappings; - } - } else { - return super.getRealmRoleMappings(); - } - } - - @Override - public Set getClientRoleMappings(ClientModel client) { - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - if (roleContainer.equals(client)) { - Set ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer); - - if (mode == Mode.LDAP_ONLY) { - // Use just role mappings from LDAP - return ldapRoleMappings; - } else { - // Merge mappings from both DB and LDAP - Set modelRoleMappings = super.getClientRoleMappings(client); - ldapRoleMappings.addAll(modelRoleMappings); - return ldapRoleMappings; - } - } else { - return super.getClientRoleMappings(client); - } - } - - @Override - public boolean hasRole(RoleModel role) { - Set roles = getRoleMappings(); - return KeycloakModelUtils.hasRole(roles, role); - } - - @Override - public void grantRole(RoleModel role) { - if (mode == Mode.LDAP_ONLY) { - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - - if (role.getContainer().equals(roleContainer)) { - - // We need to create new role mappings in LDAP - cachedLDAPRoleMappings = null; - addRoleMappingInLDAP(mapperModel, role.getName(), ldapProvider, ldapUser); - } else { - super.grantRole(role); - } - } else { - super.grantRole(role); - } - } - - @Override - public Set getRoleMappings() { - Set modelRoleMappings = super.getRoleMappings(); - - RoleContainerModel targetRoleContainer = getTargetRoleContainer(mapperModel, realm); - Set ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, targetRoleContainer); - - if (mode == Mode.LDAP_ONLY) { - // For LDAP-only we want to retrieve role mappings of target container just from LDAP - Set modelRolesCopy = new HashSet<>(modelRoleMappings); - for (RoleModel role : modelRolesCopy) { - if (role.getContainer().equals(targetRoleContainer)) { - modelRoleMappings.remove(role); - } - } - } - - modelRoleMappings.addAll(ldapRoleMappings); - return modelRoleMappings; - } - - protected Set getLDAPRoleMappingsConverted(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, RoleContainerModel roleContainer) { - if (cachedLDAPRoleMappings != null) { - return new HashSet<>(cachedLDAPRoleMappings); - } - - List ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser); - - Set roles = new HashSet<>(); - String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel); - for (LDAPObject role : ldapRoles) { - String roleName = role.getAttributeAsString(roleNameLdapAttr); - RoleModel modelRole = roleContainer.getRole(roleName); - if (modelRole == null) { - // Add role to local DB - modelRole = roleContainer.addRole(roleName); - } - roles.add(modelRole); - } - - cachedLDAPRoleMappings = new HashSet<>(roles); - - return roles; - } - - @Override - public void deleteRoleMapping(RoleModel role) { - RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); - if (role.getContainer().equals(roleContainer)) { - - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); - LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); - Condition roleNameCondition = conditionsBuilder.equal(getRoleNameLdapAttribute(mapperModel), role.getName()); - String membershipUserAttr = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel)); - Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), membershipUserAttr); - ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition); - LDAPObject ldapRole = ldapQuery.getFirstResult(); - - if (ldapRole == null) { - // Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB. - if (mode == Mode.READ_ONLY) { - super.deleteRoleMapping(role); - } - } else { - // Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error - if (mode == Mode.READ_ONLY) { - throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY"); - } else { - // Delete ldap role mappings - cachedLDAPRoleMappings = null; - deleteRoleMappingInLDAP(mapperModel, ldapProvider, ldapUser, ldapRole); - } - } - } else { - super.deleteRoleMapping(role); - } - } - } - - - public enum Mode { - /** - * All role mappings are retrieved from LDAP and saved into LDAP - */ - LDAP_ONLY, - - /** - * Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then - * they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP. - * Creating or deleting of role mapping is propagated only to DB. - * - * This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it - * won't be seen by Keycloak - */ - IMPORT, - - /** - * Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. - * Deleting role mappings, which is mapped to LDAP, will throw an error. - */ - READ_ONLY - } - - - public enum MembershipType { - - /** - * Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" ) - */ - DN, - - /** - * Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" ) - */ - UID - } -} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java index aac34f54dc..373c115e77 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java @@ -64,9 +64,12 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap"; public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap"; + public UserAttributeLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { + super(mapperModel, ldapProvider, realm); + } @Override - public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { + public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) { String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); @@ -93,7 +96,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } @Override - public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { + public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) { String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP); @@ -130,7 +133,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } } - if (isReadOnly(mapperModel)) { + if (isReadOnly()) { ldapUser.addReadOnlyAttributeName(ldapAttrName); } } @@ -151,14 +154,14 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } @Override - public UserModel proxy(UserFederationMapperModel mapperModel, final LDAPFederationProvider ldapProvider, final LDAPObject ldapUser, UserModel delegate, final RealmModel realm) { + public UserModel proxy(final LDAPObject ldapUser, UserModel delegate) { final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP); final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP); // For writable mode, we want to propagate writing of attribute to LDAP as well - if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) { + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly()) { delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) { @@ -309,13 +312,13 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } @Override - public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + public void beforeLDAPQuery(LDAPQuery query) { String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); // Add mapped attribute to returning ldap attributes query.addReturningLdapAttribute(ldapAttrName); - if (isReadOnly(mapperModel)) { + if (isReadOnly()) { query.addReturningReadOnlyLdapAttribute(ldapAttrName); } @@ -328,7 +331,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } } - private boolean isReadOnly(UserFederationMapperModel mapperModel) { + private boolean isReadOnly() { return parseBooleanParameter(mapperModel, READ_ONLY); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java index c14d0e89b3..9b061a6a44 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java @@ -1,13 +1,20 @@ package org.keycloak.federation.ldap.mappers; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.keycloak.federation.ldap.LDAPConfig; +import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.provider.ProviderConfigProperty; /** @@ -28,15 +35,15 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera configProperties.add(ldapAttribute); ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only", - "Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + "Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null); configProperties.add(readOnly); ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always Read Value From LDAP", - "If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + "If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, null); configProperties.add(alwaysReadValueFromLDAP); ProviderConfigProperty isMandatoryInLdap = createConfigProperty(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "Is Mandatory In LDAP", - "If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + "If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, null); configProperties.add(isMandatoryInLdap); } @@ -60,6 +67,20 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera return configProperties; } + @Override + public Map getDefaultConfig(UserFederationProviderModel providerModel) { + Map defaultValues = new HashMap<>(); + LDAPConfig config = new LDAPConfig(providerModel.getConfig()); + + String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true"; + defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); + + defaultValues.put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false"); + defaultValues.put(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); + + return defaultValues; + } + @Override public String getId() { return PROVIDER_ID; @@ -72,7 +93,7 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera } @Override - public UserFederationMapper create(KeycloakSession session) { - return new UserAttributeLDAPFederationMapper(); + protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) { + return new UserAttributeLDAPFederationMapper(mapperModel, federationProvider, realm); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java deleted file mode 100644 index dd7cd317c2..0000000000 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.keycloak.federation.ldap.mappers; - - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import org.keycloak.federation.ldap.LDAPFederationProvider; -import org.keycloak.federation.ldap.idm.model.LDAPDn; -import org.keycloak.federation.ldap.idm.model.LDAPObject; -import org.keycloak.federation.ldap.idm.query.Condition; -import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; -import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; -import org.keycloak.models.LDAPConstants; -import org.keycloak.models.UserFederationMapperModel; - -/** - * Strategy for how to retrieve LDAP roles of user - * - * @author Marek Posolda - */ -public enum UserRolesRetrieveStrategy { - - - /** - * Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user - */ - LOAD_ROLES_BY_MEMBER_ATTRIBUTE { - - @Override - public List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider); - String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel); - - String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel)); - - Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); - ldapQuery.addWhereCondition(membershipCondition); - return ldapQuery.getResultList(); - } - - @Override - public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { - } - - }, - - - /** - * Roles of user will be retrieved from "memberOf" attribute of our user - */ - GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE { - - @Override - public List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - Set memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF); - if (memberOfValues == null) { - return Collections.emptyList(); - } - - List roles = new LinkedList<>(); - LDAPDn parentDn = LDAPDn.fromString(roleMapper.getRolesDn(mapperModel)); - - for (String roleDn : memberOfValues) { - LDAPDn roleDN = LDAPDn.fromString(roleDn); - if (roleDN.isDescendantOf(parentDn)) { - LDAPObject role = new LDAPObject(); - role.setDn(roleDN); - - String firstDN = roleDN.getFirstRdnAttrName(); - if (firstDN.equalsIgnoreCase(roleMapper.getRoleNameLdapAttribute(mapperModel))) { - role.setRdnAttributeName(firstDN); - role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue()); - roles.add(role); - } - } - } - return roles; - } - - @Override - public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { - query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF); - query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF); - } - - }, - - - /** - * Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user. - * The query will be able to retrieve memberships recursively - * (Assume "role1" has member "role2" and role2 has member "johnuser". Then searching for roles of "johnuser" will return both "role1" and "role2" ) - * - * This is using AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers - */ - LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY { - - @Override - public List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider); - String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel); - membershipAttr = membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN; - String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel)); - - Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); - ldapQuery.addWhereCondition(membershipCondition); - return ldapQuery.getResultList(); - } - - @Override - public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { - } - - }; - - - - public abstract List getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser); - - public abstract void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query); - -} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapper.java new file mode 100644 index 0000000000..ce876c0dcc --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapper.java @@ -0,0 +1,15 @@ +package org.keycloak.federation.ldap.mappers.membership; + +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; + +/** + * Mapper related to mapping of LDAP groups to keycloak model objects (either keycloak roles or keycloak groups) + * + * @author Marek Posolda + */ +public interface CommonLDAPGroupMapper { + + LDAPQuery createLDAPGroupQuery(); + + CommonLDAPGroupMapperConfig getConfig(); +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java new file mode 100644 index 0000000000..ac9f34ce21 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java @@ -0,0 +1,70 @@ +package org.keycloak.federation.ldap.mappers.membership; + +import java.util.HashSet; +import java.util.Set; + +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.UserFederationMapperModel; + +/** + * @author Marek Posolda + */ +public abstract class CommonLDAPGroupMapperConfig { + + // Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member" + public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute"; + + // See docs for MembershipType enum + public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type"; + + // See docs for Mode enum + public static final String MODE = "mode"; + + // See docs for UserRolesRetriever enum + public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy"; + + + protected final UserFederationMapperModel mapperModel; + + public CommonLDAPGroupMapperConfig(UserFederationMapperModel mapperModel) { + this.mapperModel = mapperModel; + } + + public String getMembershipLdapAttribute() { + String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE); + return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER; + } + + public MembershipType getMembershipTypeLdapAttribute() { + String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE); + return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; + } + + public LDAPGroupMapperMode getMode() { + String modeString = mapperModel.getConfig().get(MODE); + if (modeString == null || modeString.isEmpty()) { + throw new ModelException("Mode is missing! Check your configuration"); + } + + return Enum.valueOf(LDAPGroupMapperMode.class, modeString.toUpperCase()); + } + + protected Set getConfigValues(String str) { + String[] objClasses = str.split(","); + Set trimmed = new HashSet<>(); + for (String objectClass : objClasses) { + objectClass = objectClass.trim(); + if (objectClass.length() > 0) { + trimmed.add(objectClass); + } + } + return trimmed; + } + + public abstract String getLDAPGroupsDn(); + + public abstract String getLDAPGroupNameLdapAttribute(); + + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/LDAPGroupMapperMode.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/LDAPGroupMapperMode.java new file mode 100644 index 0000000000..d9fa0f0e9d --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/LDAPGroupMapperMode.java @@ -0,0 +1,29 @@ +package org.keycloak.federation.ldap.mappers.membership; + +/** + * @author Marek Posolda + */ +public enum LDAPGroupMapperMode { + + /** + * All role mappings are retrieved from LDAP and saved into LDAP + */ + LDAP_ONLY, + + /** + * Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then + * they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP. + * Creating or deleting of role mapping is propagated only to DB. + * + * This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it + * won't be seen by Keycloak + */ + IMPORT, + + /** + * Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. + * Deleting role mappings, which is mapped to LDAP, will throw an error. + */ + READ_ONLY + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java new file mode 100644 index 0000000000..624ed3b07d --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java @@ -0,0 +1,17 @@ +package org.keycloak.federation.ldap.mappers.membership; + +/** + * @author Marek Posolda + */ +public enum MembershipType { + + /** + * Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" ) + */ + DN, + + /** + * Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" ) + */ + UID +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/UserRolesRetrieveStrategy.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/UserRolesRetrieveStrategy.java new file mode 100644 index 0000000000..dfba2f283a --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/UserRolesRetrieveStrategy.java @@ -0,0 +1,111 @@ +package org.keycloak.federation.ldap.mappers.membership; + + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPDn; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.models.LDAPConstants; + +/** + * Strategy for how to retrieve LDAP roles of user + * + * @author Marek Posolda + */ +public interface UserRolesRetrieveStrategy { + + + List getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser); + + void beforeUserLDAPQuery(LDAPQuery query); + + + // Impl subclasses + + /** + * Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user + */ + class LoadRolesByMember implements UserRolesRetrieveStrategy { + + @Override + public List getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) { + LDAPQuery ldapQuery = roleOrGroupMapper.createLDAPGroupQuery(); + String membershipAttr = roleOrGroupMapper.getConfig().getMembershipLdapAttribute(); + + String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute()); + + Condition membershipCondition = getMembershipCondition(membershipAttr, userMembership); + ldapQuery.addWhereCondition(membershipCondition); + return ldapQuery.getResultList(); + } + + @Override + public void beforeUserLDAPQuery(LDAPQuery query) { + } + + protected Condition getMembershipCondition(String membershipAttr, String userMembership) { + return new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); + } + + }; + + /** + * Roles of user will be retrieved from "memberOf" attribute of our user + */ + class GetRolesFromUserMemberOfAttribute implements UserRolesRetrieveStrategy { + + @Override + public List getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) { + Set memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF); + if (memberOfValues == null) { + return Collections.emptyList(); + } + + List roles = new LinkedList<>(); + LDAPDn parentDn = LDAPDn.fromString(roleOrGroupMapper.getConfig().getLDAPGroupsDn()); + + for (String roleDn : memberOfValues) { + LDAPDn roleDN = LDAPDn.fromString(roleDn); + if (roleDN.isDescendantOf(parentDn)) { + LDAPObject role = new LDAPObject(); + role.setDn(roleDN); + + String firstDN = roleDN.getFirstRdnAttrName(); + if (firstDN.equalsIgnoreCase(roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute())) { + role.setRdnAttributeName(firstDN); + role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue()); + roles.add(role); + } + } + } + return roles; + } + + @Override + public void beforeUserLDAPQuery(LDAPQuery query) { + query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF); + query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF); + } + + }; + + /** + * Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user. + * The query will be able to retrieve memberships recursively with usage of AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers + */ + class LoadRolesByMemberRecursively extends LoadRolesByMember { + + protected Condition getMembershipCondition(String membershipAttr, String userMembership) { + return new LDAPQueryConditionsBuilder().equal(membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN, userMembership); + } + + }; + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java new file mode 100644 index 0000000000..5115e9ae17 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java @@ -0,0 +1,634 @@ +package org.keycloak.federation.ldap.mappers.membership.group; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPDn; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapper; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.MembershipType; +import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy; +import org.keycloak.models.GroupModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.UserModelDelegate; + +/** + * @author Marek Posolda + */ +public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper implements CommonLDAPGroupMapper { + + private static final Logger logger = Logger.getLogger(GroupLDAPFederationMapper.class); + + private final GroupMapperConfig config; + private final GroupLDAPFederationMapperFactory factory; + + // Flag to avoid syncing multiple times per transaction + private boolean syncFromLDAPPerformedInThisTransaction = false; + + public GroupLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm, GroupLDAPFederationMapperFactory factory) { + super(mapperModel, ldapProvider, realm); + this.config = new GroupMapperConfig(mapperModel); + this.factory = factory; + } + + + // CommonLDAPGroupMapper interface + + @Override + public LDAPQuery createLDAPGroupQuery() { + return createGroupQuery(); + } + + @Override + public CommonLDAPGroupMapperConfig getConfig() { + return config; + } + + + + // LDAP Group CRUD operations + + public LDAPQuery createGroupQuery() { + LDAPQuery ldapQuery = new LDAPQuery(ldapProvider); + + // For now, use same search scope, which is configured "globally" and used for user's search. + ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope()); + + String groupsDn = config.getGroupsDn(); + ldapQuery.setSearchDn(groupsDn); + + Collection groupObjectClasses = config.getGroupObjectClasses(ldapProvider); + ldapQuery.addObjectClasses(groupObjectClasses); + + String customFilter = config.getCustomLdapFilter(); + if (customFilter != null && customFilter.trim().length() > 0) { + Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter); + ldapQuery.addWhereCondition(customFilterCondition); + } + + ldapQuery.addReturningLdapAttribute(config.getGroupNameLdapAttribute()); + ldapQuery.addReturningLdapAttribute(config.getMembershipLdapAttribute()); + + for (String groupAttr : config.getGroupAttributes()) { + ldapQuery.addReturningLdapAttribute(groupAttr); + } + + return ldapQuery; + } + + public LDAPObject createLDAPGroup(String groupName, Map> additionalAttributes) { + LDAPObject ldapGroup = LDAPUtils.createLDAPGroup(ldapProvider, groupName, config.getGroupNameLdapAttribute(), config.getGroupObjectClasses(ldapProvider), + config.getGroupsDn(), additionalAttributes); + + logger.debugf("Creating group [%s] to LDAP with DN [%s]", groupName, ldapGroup.getDn().toString()); + return ldapGroup; + } + + public LDAPObject loadLDAPGroupByName(String groupName) { + LDAPQuery ldapQuery = createGroupQuery(); + Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getGroupNameLdapAttribute(), groupName); + ldapQuery.addWhereCondition(roleNameCondition); + return ldapQuery.getFirstResult(); + } + + protected Set getLDAPSubgroups(LDAPObject ldapGroup) { + return getLDAPMembersWithParent(ldapGroup, LDAPDn.fromString(config.getGroupsDn())); + } + + // Get just those members of specified group, which are descendants of "requiredParentDn" + protected Set getLDAPMembersWithParent(LDAPObject ldapGroup, LDAPDn requiredParentDn) { + Set allMemberships = LDAPUtils.getExistingMemberships(config.getMembershipLdapAttribute(), ldapGroup); + + // Filter and keep just groups + Set result = new HashSet<>(); + for (String membership : allMemberships) { + LDAPDn childDn = LDAPDn.fromString(membership); + if (childDn.isDescendantOf(requiredParentDn)) { + result.add(childDn); + } + } + return result; + } + + + // Sync from Ldap to KC + + public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() { + UserFederationSyncResult syncResult = new UserFederationSyncResult() { + + @Override + public String getStatus() { + return String.format("%d imported groups, %d updated groups, %d removed groups", getAdded(), getUpdated(), getRemoved()); + } + + }; + + logger.debugf("Syncing groups from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); + + // Get all LDAP groups + LDAPQuery ldapQuery = createGroupQuery(); + List ldapGroups = ldapQuery.getResultList(); + + // Convert to internal format + Map ldapGroupsMap = new HashMap<>(); + List ldapGroupsRep = new LinkedList<>(); + + String groupsRdnAttr = config.getGroupNameLdapAttribute(); + for (LDAPObject ldapGroup : ldapGroups) { + String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr); + + Set subgroupNames = new HashSet<>(); + for (LDAPDn groupDn : getLDAPSubgroups(ldapGroup)) { + subgroupNames.add(groupDn.getFirstRdnAttrValue()); + } + + ldapGroupsRep.add(new GroupTreeResolver.Group(groupName, subgroupNames)); + ldapGroupsMap.put(groupName, ldapGroup); + } + + // Now we have list of LDAP groups. Let's form the tree (if needed) + if (config.isPreserveGroupsInheritance()) { + try { + List groupTrees = new GroupTreeResolver().resolveGroupTree(ldapGroupsRep); + + updateKeycloakGroupTree(groupTrees, ldapGroupsMap, syncResult); + } catch (GroupTreeResolver.GroupTreeResolveException gre) { + throw new ModelException("Couldn't resolve groups from LDAP. Fix LDAP or skip preserve inheritance. Details: " + gre.getMessage(), gre); + } + } else { + Set visitedGroupIds = new HashSet<>(); + + // Just add flat structure of groups with all groups at top-level + for (Map.Entry groupEntry : ldapGroupsMap.entrySet()) { + String groupName = groupEntry.getKey(); + GroupModel kcExistingGroup = KeycloakModelUtils.findGroupByPath(realm, "/" + groupName); + + if (kcExistingGroup != null) { + updateAttributesOfKCGroup(kcExistingGroup, groupEntry.getValue()); + syncResult.increaseUpdated(); + visitedGroupIds.add(kcExistingGroup.getId()); + } else { + GroupModel kcGroup = realm.createGroup(groupName); + updateAttributesOfKCGroup(kcGroup, groupEntry.getValue()); + realm.moveGroup(kcGroup, null); + syncResult.increaseAdded(); + visitedGroupIds.add(kcGroup.getId()); + } + } + + // Possibly remove keycloak groups, which doesn't exists in LDAP + if (config.isDropNonExistingGroupsDuringSync()) { + dropNonExistingKcGroups(syncResult, visitedGroupIds); + } + } + + syncFromLDAPPerformedInThisTransaction = true; + + return syncResult; + } + + private void updateKeycloakGroupTree(List groupTrees, Map ldapGroups, UserFederationSyncResult syncResult) { + Set visitedGroupIds = new HashSet<>(); + + for (GroupTreeResolver.GroupTreeEntry groupEntry : groupTrees) { + updateKeycloakGroupTreeEntry(groupEntry, ldapGroups, null, syncResult, visitedGroupIds); + } + + // Possibly remove keycloak groups, which doesn't exists in LDAP + if (config.isDropNonExistingGroupsDuringSync()) { + dropNonExistingKcGroups(syncResult, visitedGroupIds); + } + } + + private void updateKeycloakGroupTreeEntry(GroupTreeResolver.GroupTreeEntry groupTreeEntry, Map ldapGroups, GroupModel kcParent, UserFederationSyncResult syncResult, Set visitedGroupIds) { + String groupName = groupTreeEntry.getGroupName(); + + // Check if group already exists + GroupModel kcGroup = null; + Collection subgroups = kcParent == null ? realm.getTopLevelGroups() : kcParent.getSubGroups(); + for (GroupModel group : subgroups) { + if (group.getName().equals(groupName)) { + kcGroup = group; + break; + } + } + + if (kcGroup != null) { + logger.infof("Updated Keycloak group '%s' from LDAP", kcGroup.getName()); + updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName())); + syncResult.increaseUpdated(); + } else { + kcGroup = realm.createGroup(groupTreeEntry.getGroupName()); + if (kcParent == null) { + realm.moveGroup(kcGroup, null); + logger.infof("Imported top-level group '%s' from LDAP", kcGroup.getName()); + } else { + realm.moveGroup(kcGroup, kcParent); + logger.infof("Imported group '%s' from LDAP as child of group '%s'", kcGroup.getName(), kcParent.getName()); + } + + updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName())); + syncResult.increaseAdded(); + } + + visitedGroupIds.add(kcGroup.getId()); + + for (GroupTreeResolver.GroupTreeEntry childEntry : groupTreeEntry.getChildren()) { + updateKeycloakGroupTreeEntry(childEntry, ldapGroups, kcGroup, syncResult, visitedGroupIds); + } + } + + private void dropNonExistingKcGroups(UserFederationSyncResult syncResult, Set visitedGroupIds) { + // Remove keycloak groups, which doesn't exists in LDAP + List allGroups = realm.getGroups(); + for (GroupModel kcGroup : allGroups) { + if (!visitedGroupIds.contains(kcGroup.getId())) { + logger.infof("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName()); + realm.removeGroup(kcGroup); + syncResult.increaseRemoved(); + } + } + } + + private void updateAttributesOfKCGroup(GroupModel kcGroup, LDAPObject ldapGroup) { + Collection groupAttributes = config.getGroupAttributes(); + + for (String attrName : groupAttributes) { + Set attrValues = ldapGroup.getAttributeAsSet(attrName); + if (attrValues==null) { + kcGroup.removeAttribute(attrName); + } else { + kcGroup.setAttribute(attrName, new LinkedList<>(attrValues)); + } + } + } + + // Override if better effectivity or different algorithm is needed + protected GroupModel findKcGroupByLDAPGroup(LDAPObject ldapGroup) { + String groupNameAttr = config.getGroupNameLdapAttribute(); + String groupName = ldapGroup.getAttributeAsString(groupNameAttr); + + List groups = realm.getGroups(); + for (GroupModel group : groups) { + if (group.getName().equals(groupName)) { + return group; + } + } + + return null; + } + + protected GroupModel findKcGroupOrSyncFromLDAP(LDAPObject ldapGroup, UserModel user) { + GroupModel kcGroup = findKcGroupByLDAPGroup(ldapGroup); + + if (kcGroup == null) { + // Sync groups from LDAP + if (!syncFromLDAPPerformedInThisTransaction) { + syncDataFromFederationProviderToKeycloak(); + kcGroup = findKcGroupByLDAPGroup(ldapGroup); + } + + // Could theoretically happen on some LDAP servers if 'memberof' style is used and 'memberof' attribute of user references non-existing group + if (kcGroup == null) { + String groupName = ldapGroup.getAttributeAsString(config.getGroupNameLdapAttribute()); + logger.warnf("User '%s' is member of group '%s', which doesn't exists in LDAP", user.getUsername(), groupName); + } + } + + return kcGroup; + } + + + // Sync from Keycloak to LDAP + + public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() { + UserFederationSyncResult syncResult = new UserFederationSyncResult() { + + @Override + public String getStatus() { + return String.format("%d groups imported to LDAP, %d groups updated to LDAP, %d groups removed from LDAP", getAdded(), getUpdated(), getRemoved()); + } + + }; + + if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) { + logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString()); + return syncResult; + } + + logger.debugf("Syncing groups from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); + + // Query existing LDAP groups + LDAPQuery ldapQuery = createGroupQuery(); + List ldapGroups = ldapQuery.getResultList(); + + // Convert them to Map + Map ldapGroupsMap = new HashMap<>(); + String groupsRdnAttr = config.getGroupNameLdapAttribute(); + for (LDAPObject ldapGroup : ldapGroups) { + String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr); + ldapGroupsMap.put(groupName, ldapGroup); + } + + // Map to track all LDAP groups also exists in Keycloak + Set ldapGroupNames = new HashSet<>(); + + // Create or update KC groups to LDAP including their attributes + for (GroupModel kcGroup : realm.getTopLevelGroups()) { + processLdapGroupSyncToLDAP(kcGroup, ldapGroupsMap, ldapGroupNames, syncResult); + } + + // If dropNonExisting, then drop all groups, which doesn't exist in KC from LDAP as well + if (config.isDropNonExistingGroupsDuringSync()) { + Set copy = new HashSet<>(ldapGroupsMap.keySet()); + for (String groupName : copy) { + if (!ldapGroupNames.contains(groupName)) { + LDAPObject ldapGroup = ldapGroupsMap.remove(groupName); + ldapProvider.getLdapIdentityStore().remove(ldapGroup); + syncResult.increaseRemoved(); + } + } + } + + // Finally process memberships, + if (config.isPreserveGroupsInheritance()) { + for (GroupModel kcGroup : realm.getTopLevelGroups()) { + processLdapGroupMembershipsSyncToLDAP(kcGroup, ldapGroupsMap); + } + } + + return syncResult; + } + + // For given kcGroup check if it exists in LDAP (map) by name + // If not, create it in LDAP including attributes. Otherwise update attributes in LDAP. + // Process this recursively for all subgroups of KC group + private void processLdapGroupSyncToLDAP(GroupModel kcGroup, Map ldapGroupsMap, Set ldapGroupNames, UserFederationSyncResult syncResult) { + String groupName = kcGroup.getName(); + + // extract group attributes to be updated to LDAP + Map> supportedLdapAttributes = new HashMap<>(); + for (String attrName : config.getGroupAttributes()) { + List kcAttrValues = kcGroup.getAttribute(attrName); + Set attrValues2 = (kcAttrValues == null || kcAttrValues.isEmpty()) ? null : new HashSet<>(kcAttrValues); + supportedLdapAttributes.put(attrName, attrValues2); + } + + LDAPObject ldapGroup = ldapGroupsMap.get(groupName); + + if (ldapGroup == null) { + ldapGroup = createLDAPGroup(groupName, supportedLdapAttributes); + syncResult.increaseAdded(); + } else { + for (Map.Entry> attrEntry : supportedLdapAttributes.entrySet()) { + ldapGroup.setAttribute(attrEntry.getKey(), attrEntry.getValue()); + } + + ldapProvider.getLdapIdentityStore().update(ldapGroup); + syncResult.increaseUpdated(); + } + + ldapGroupsMap.put(groupName, ldapGroup); + ldapGroupNames.add(groupName); + + // process KC subgroups + for (GroupModel kcSubgroup : kcGroup.getSubGroups()) { + processLdapGroupSyncToLDAP(kcSubgroup, ldapGroupsMap, ldapGroupNames, syncResult); + } + } + + // Sync memberships update. Update memberships of group in LDAP based on subgroups from KC. Do it recursively + private void processLdapGroupMembershipsSyncToLDAP(GroupModel kcGroup, Map ldapGroupsMap) { + LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName()); + Set toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup); + + // Add LDAP subgroups, which are KC subgroups + Set kcSubgroups = kcGroup.getSubGroups(); + for (GroupModel kcSubgroup : kcSubgroups) { + LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName()); + LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, ldapSubgroup, false); + toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn()); + } + + // Remove LDAP subgroups, which are not members in KC anymore + for (LDAPDn toRemoveDN : toRemoveSubgroupsDNs) { + LDAPObject fakeGroup = new LDAPObject(); + fakeGroup.setDn(toRemoveDN); + LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, fakeGroup, false); + } + + // Update group to LDAP + if (!kcGroup.getSubGroups().isEmpty() || !toRemoveSubgroupsDNs.isEmpty()) { + ldapProvider.getLdapIdentityStore().update(ldapGroup); + } + + for (GroupModel kcSubgroup : kcGroup.getSubGroups()) { + processLdapGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap); + } + } + + + // group-user membership operations + + + @Override + public List getGroupMembers(GroupModel kcGroup, int firstResult, int maxResults) { + LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName()); + if (ldapGroup == null) { + return Collections.emptyList(); + } + + LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); + Set userDns = getLDAPMembersWithParent(ldapGroup, usersDn); + + if (userDns == null) { + return Collections.emptyList(); + } + + if (userDns.size() <= firstResult) { + return Collections.emptyList(); + } + + List dns = new ArrayList<>(userDns); + int max = Math.min(dns.size(), firstResult + maxResults); + dns = dns.subList(firstResult, max); + + // We have dns of users, who are members of our group. Load them now + return ldapProvider.loadUsersByLDAPDns(dns, realm); + } + + public void addGroupMappingInLDAP(String groupName, LDAPObject ldapUser) { + LDAPObject ldapGroup = loadLDAPGroupByName(groupName); + if (ldapGroup == null) { + syncDataFromKeycloakToFederationProvider(); + ldapGroup = loadLDAPGroupByName(groupName); + } + + LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true); + } + + public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) { + LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true); + } + + protected List getLDAPGroupMappings(LDAPObject ldapUser) { + String strategyKey = config.getUserGroupsRetrieveStrategy(); + UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey); + return strategy.getLDAPRoleMappings(this, ldapUser); + } + + public void beforeLDAPQuery(LDAPQuery query) { + String strategyKey = config.getUserGroupsRetrieveStrategy(); + UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey); + strategy.beforeUserLDAPQuery(query); + } + + public UserModel proxy(LDAPObject ldapUser, UserModel delegate) { + final LDAPGroupMapperMode mode = config.getMode(); + + // For IMPORT mode, all operations are performed against local DB + if (mode == LDAPGroupMapperMode.IMPORT) { + return delegate; + } else { + return new LDAPGroupMappingsUserDelegate(delegate, ldapUser); + } + } + + public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) { + } + + public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) { + LDAPGroupMapperMode mode = config.getMode(); + + // For now, import LDAP group mappings just during create + if (mode == LDAPGroupMapperMode.IMPORT && isCreate) { + + List ldapGroups = getLDAPGroupMappings(ldapUser); + + // Import role mappings from LDAP into Keycloak DB + for (LDAPObject ldapGroup : ldapGroups) { + + GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, user); + if (kcGroup != null) { + logger.infof("User [%s] joins group [%s] during import from LDAP", user.getUsername(), kcGroup.getName()); + user.joinGroup(kcGroup); + } + } + } + } + + + public class LDAPGroupMappingsUserDelegate extends UserModelDelegate { + + private final LDAPObject ldapUser; + + // Avoid loading group mappings from LDAP more times per-request + private Set cachedLDAPGroupMappings; + + public LDAPGroupMappingsUserDelegate(UserModel user, LDAPObject ldapUser) { + super(user); + this.ldapUser = ldapUser; + } + + @Override + public Set getGroups() { + Set ldapGroupMappings = getLDAPGroupMappingsConverted(); + if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) { + // Use just group mappings from LDAP + return ldapGroupMappings; + } else { + // Merge mappings from both DB and LDAP + Set modelGroupMappings = super.getGroups(); + ldapGroupMappings.addAll(modelGroupMappings); + return ldapGroupMappings; + } + } + + @Override + public void joinGroup(GroupModel group) { + if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) { + // We need to create new role mappings in LDAP + cachedLDAPGroupMappings = null; + addGroupMappingInLDAP(group.getName(), ldapUser); + } else { + super.joinGroup(group); + } + } + + @Override + public void leaveGroup(GroupModel group) { + LDAPQuery ldapQuery = createGroupQuery(); + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName()); + String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute()); + Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr); + ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition); + LDAPObject ldapGroup = ldapQuery.getFirstResult(); + + if (ldapGroup == null) { + // Group mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB. + if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) { + super.leaveGroup(group); + } + } else { + // Group mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error + if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) { + throw new ModelException("Not possible to delete LDAP group mappings as mapper mode is READ_ONLY"); + } else { + // Delete ldap role mappings + cachedLDAPGroupMappings = null; + deleteGroupMappingInLDAP(ldapUser, ldapGroup); + } + } + } + + @Override + public boolean isMemberOf(GroupModel group) { + Set ldapGroupMappings = getGroups(); + return ldapGroupMappings.contains(group); + } + + protected Set getLDAPGroupMappingsConverted() { + if (cachedLDAPGroupMappings != null) { + return new HashSet<>(cachedLDAPGroupMappings); + } + + List ldapGroups = getLDAPGroupMappings(ldapUser); + + Set result = new HashSet<>(); + for (LDAPObject ldapGroup : ldapGroups) { + GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, this); + if (kcGroup != null) { + result.add(kcGroup); + } + } + + cachedLDAPGroupMappings = new HashSet<>(result); + + return result; + } + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java new file mode 100644 index 0000000000..1e06fa995b --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java @@ -0,0 +1,183 @@ +package org.keycloak.federation.ldap.mappers.membership.group; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.keycloak.federation.ldap.LDAPConfig; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.MembershipType; +import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy; +import org.keycloak.federation.ldap.mappers.membership.role.RoleMapperConfig; +import org.keycloak.mappers.MapperConfigValidationException; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; + +/** + * @author Marek Posolda + */ +public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { + + public static final String PROVIDER_ID = "group-ldap-mapper"; + + protected static final List configProperties = new ArrayList<>(); + protected static final Map userGroupsStrategies = new LinkedHashMap<>(); + + // TODO: Merge with RoleLDAPFederationMapperFactory as there are lot of similar properties + static { + userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember()); + userGroupsStrategies.put(GroupMapperConfig.GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute()); + userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively()); + + ProviderConfigProperty groupsDn = createConfigProperty(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", + "LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(groupsDn); + + ProviderConfigProperty groupNameLDAPAttribute = createConfigProperty(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE, "Group Name LDAP Attribute", + "Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ", + ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(groupNameLDAPAttribute); + + ProviderConfigProperty groupObjectClasses = createConfigProperty(GroupMapperConfig.GROUP_OBJECT_CLASSES, "Group Object Classes", + "Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ", + ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(groupObjectClasses); + + ProviderConfigProperty preserveGroupInheritance = createConfigProperty(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "Preserve Group Inheritance", + "Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " + + "preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups", + ProviderConfigProperty.BOOLEAN_TYPE, null); + configProperties.add(preserveGroupInheritance); + + ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute", + "Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ", + ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(membershipLDAPAttribute); + + List membershipTypes = new LinkedList<>(); + for (MembershipType membershipType : MembershipType.values()) { + membershipTypes.add(membershipType.toString()); + } + ProviderConfigProperty membershipType = createConfigProperty(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", + "DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " + + "UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .", + ProviderConfigProperty.LIST_TYPE, membershipTypes); + configProperties.add(membershipType); + + ProviderConfigProperty ldapFilter = createConfigProperty(GroupMapperConfig.GROUPS_LDAP_FILTER, + "LDAP Filter", + "LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'", + ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(ldapFilter); + + List modes = new LinkedList<>(); + for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) { + modes.add(mode.toString()); + } + ProviderConfigProperty mode = createConfigProperty(GroupMapperConfig.MODE, "Mode", + "LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " + + "retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are " + + "retrieved from LDAP just at the time when user is imported from LDAP and then " + + "they are saved to local keycloak DB.", + ProviderConfigProperty.LIST_TYPE, modes); + configProperties.add(mode); + + List roleRetrievers = new LinkedList<>(userGroupsStrategies.keySet()); + ProviderConfigProperty retriever = createConfigProperty(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, "User Groups Retrieve Strategy", + "Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " + + "GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " + + "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension." + , + ProviderConfigProperty.LIST_TYPE, roleRetrievers); + configProperties.add(retriever); + + ProviderConfigProperty mappedGroupAttributes = createConfigProperty(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES, "Mapped Group Attributes", + "List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " + + "Leave this empty if no additional group attributes are required to be mapped in Keycloak. ", + ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(mappedGroupAttributes); + + ProviderConfigProperty dropNonExistingGroupsDuringSync = createConfigProperty(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "Drop non-existing groups during sync", + "If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted", + ProviderConfigProperty.BOOLEAN_TYPE, null); + configProperties.add(dropNonExistingGroupsDuringSync); + } + + @Override + public String getHelpText() { + return "Used to map group mappings of groups from some LDAP DN to Keycloak group mappings"; + } + + @Override + public String getDisplayCategory() { + return GROUP_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Group mappings"; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public Map getDefaultConfig(UserFederationProviderModel providerModel) { + Map defaultValues = new HashMap<>(); + LDAPConfig config = new LDAPConfig(providerModel.getConfig()); + + defaultValues.put(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE, LDAPConstants.CN); + + String roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES; + defaultValues.put(GroupMapperConfig.GROUP_OBJECT_CLASSES, roleObjectClasses); + + defaultValues.put(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true"); + defaultValues.put(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, LDAPConstants.MEMBER); + + String mode = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString(); + defaultValues.put(GroupMapperConfig.MODE, mode); + defaultValues.put(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE); + + defaultValues.put(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "false"); + + return defaultValues; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public UserFederationMapperSyncConfigRepresentation getSyncConfig() { + return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-groups-to-keycloak", true, "sync-keycloak-groups-to-ldap"); + } + + @Override + public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", mapperModel); + checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", mapperModel); + } + + @Override + protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) { + return new GroupLDAPFederationMapper(mapperModel, federationProvider, realm, this); + } + + protected UserRolesRetrieveStrategy getUserGroupsRetrieveStrategy(String strategyKey) { + return userGroupsStrategies.get(strategyKey); + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupMapperConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupMapperConfig.java new file mode 100644 index 0000000000..b3f5691dca --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupMapperConfig.java @@ -0,0 +1,108 @@ +package org.keycloak.federation.ldap.mappers.membership.group; + +import java.util.Collection; +import java.util.Collections; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.UserFederationMapperModel; + +/** + * @author Marek Posolda + */ +public class GroupMapperConfig extends CommonLDAPGroupMapperConfig { + + // LDAP DN where are groups of this tree saved. + public static final String GROUPS_DN = "groups.dn"; + + // Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be "cn" + public static final String GROUP_NAME_LDAP_ATTRIBUTE = "group.name.ldap.attribute"; + + // Object classes of the group object. + public static final String GROUP_OBJECT_CLASSES = "group.object.classes"; + + // Flag whether group inheritance from LDAP should be propagated to Keycloak group inheritance. + public static final String PRESERVE_GROUP_INHERITANCE = "preserve.group.inheritance"; + + // Customized LDAP filter which is added to the whole LDAP query + public static final String GROUPS_LDAP_FILTER = "groups.ldap.filter"; + + // Name of attributes of the LDAP group object, which will be mapped as attributes of Group in Keycloak + public static final String MAPPED_GROUP_ATTRIBUTES = "mapped.group.attributes"; + + // During sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted + public static final String DROP_NON_EXISTING_GROUPS_DURING_SYNC = "drop.non.existing.groups.during.sync"; + + // See UserRolesRetrieveStrategy + public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"; + public static final String GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE"; + public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY"; + + public GroupMapperConfig(UserFederationMapperModel mapperModel) { + super(mapperModel); + } + + + public String getGroupsDn() { + String groupsDn = mapperModel.getConfig().get(GROUPS_DN); + if (groupsDn == null) { + throw new ModelException("Groups DN is null! Check your configuration"); + } + return groupsDn; + } + + @Override + public String getLDAPGroupsDn() { + return getGroupsDn(); + } + + public String getGroupNameLdapAttribute() { + String rolesRdnAttr = mapperModel.getConfig().get(GROUP_NAME_LDAP_ATTRIBUTE); + return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN; + } + + @Override + public String getLDAPGroupNameLdapAttribute() { + return getGroupNameLdapAttribute(); + } + + public boolean isPreserveGroupsInheritance() { + return AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, PRESERVE_GROUP_INHERITANCE); + } + + public String getMembershipLdapAttribute() { + String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE); + return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER; + } + + public Collection getGroupObjectClasses(LDAPFederationProvider ldapProvider) { + String objectClasses = mapperModel.getConfig().get(GROUP_OBJECT_CLASSES); + if (objectClasses == null) { + // For Active directory, the default is 'group' . For other servers 'groupOfNames' + objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES; + } + + return getConfigValues(objectClasses); + } + + public Collection getGroupAttributes() { + String groupAttrs = mapperModel.getConfig().get(MAPPED_GROUP_ATTRIBUTES); + return (groupAttrs == null) ? Collections.emptySet() : getConfigValues(groupAttrs); + } + + public String getCustomLdapFilter() { + return mapperModel.getConfig().get(GROUPS_LDAP_FILTER); + } + + public boolean isDropNonExistingGroupsDuringSync() { + return AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, DROP_NON_EXISTING_GROUPS_DURING_SYNC); + } + + public String getUserGroupsRetrieveStrategy() { + String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY); + return strategyString!=null ? strategyString : LOAD_GROUPS_BY_MEMBER_ATTRIBUTE; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupTreeResolver.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupTreeResolver.java new file mode 100644 index 0000000000..74c8739de7 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupTreeResolver.java @@ -0,0 +1,187 @@ +package org.keycloak.federation.ldap.mappers.membership.group; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * @author Marek Posolda + */ +public class GroupTreeResolver { + + + /** + * Fully resolves list of group trees to be used in Keycloak. The input is group info (usually from LDAP) where each "Group" object contains + * just it's name and direct children. + * + * The operation also performs validation as rules for LDAP are less strict than for Keycloak (In LDAP, the recursion is possible and multiple parents of single group is also allowed) + * + * @param groups + * @return + * @throws GroupTreeResolveException + */ + public List resolveGroupTree(List groups) throws GroupTreeResolveException { + // 1- Get parents of each group + Map> parentsTree = getParentsTree(groups); + + // 2 - Get rootGroups (groups without parent) and check if there is no group with multiple parents + List rootGroups = new LinkedList<>(); + for (Map.Entry> group : parentsTree.entrySet()) { + int parentCount = group.getValue().size(); + if (parentCount == 0) { + rootGroups.add(group.getKey()); + } else if (parentCount > 1) { + throw new GroupTreeResolveException("Group '" + group.getKey() + "' detected to have multiple parents. This is not allowed in Keycloak. Parents are: " + group.getValue()); + } + } + + // 3 - Just convert to map for easier retrieval + Map asMap = new TreeMap<>(); + for (Group group : groups) { + asMap.put(group.getGroupName(), group); + } + + // 4 - Now we have rootGroups. Let's resolve them + List finalResult = new LinkedList<>(); + Set visitedGroups = new TreeSet<>(); + for (String rootGroupName : rootGroups) { + List subtree = new LinkedList<>(); + subtree.add(rootGroupName); + GroupTreeEntry groupTree = resolveGroupTree(rootGroupName, asMap, visitedGroups, subtree); + finalResult.add(groupTree); + } + + + // 5 - Check recursion + if (visitedGroups.size() != asMap.size()) { + // Recursion detected. Try to find where it is + for (Map.Entry entry : asMap.entrySet()) { + String groupName = entry.getKey(); + if (!visitedGroups.contains(groupName)) { + List subtree = new LinkedList<>(); + subtree.add(groupName); + + Set newVisitedGroups = new TreeSet<>(); + resolveGroupTree(groupName, asMap, newVisitedGroups, subtree); + visitedGroups.addAll(newVisitedGroups); + } + } + + // Shouldn't happen + throw new GroupTreeResolveException("Illegal state: Recursion detected, but wasn't able to find it"); + } + + return finalResult; + } + + private Map> getParentsTree(List groups) throws GroupTreeResolveException { + Map> result = new TreeMap<>(); + + for (Group group : groups) { + result.put(group.getGroupName(), new LinkedList()); + } + + for (Group group : groups) { + for (String child : group.getChildrenNames()) { + List list = result.get(child); + if (list == null) { + throw new GroupTreeResolveException("Group '" + child + "' referenced as member of group '" + group.getGroupName() + "' doesn't exists"); + } + list.add(group.getGroupName()); + } + } + return result; + } + + private GroupTreeEntry resolveGroupTree(String groupName, Map asMap, Set visitedGroups, List currentSubtree) throws GroupTreeResolveException { + if (visitedGroups.contains(groupName)) { + throw new GroupTreeResolveException("Recursion detected when trying to resolve group '" + groupName + "'. Whole recursion path: " + currentSubtree); + } + + visitedGroups.add(groupName); + + Group group = asMap.get(groupName); + + List children = new LinkedList<>(); + GroupTreeEntry result = new GroupTreeEntry(group.getGroupName(), children); + + for (String childrenName : group.getChildrenNames()) { + List subtreeCopy = new LinkedList<>(currentSubtree); + subtreeCopy.add(childrenName); + GroupTreeEntry childEntry = resolveGroupTree(childrenName, asMap, visitedGroups, subtreeCopy); + children.add(childEntry); + } + + return result; + } + + + + // static classes + + public static class GroupTreeResolveException extends Exception { + + public GroupTreeResolveException(String message) { + super(message); + } + } + + + public static class Group { + + private final String groupName; + private final List childrenNames; + + public Group(String groupName, String... childrenNames) { + this(groupName, Arrays.asList(childrenNames)); + } + + public Group(String groupName, Collection childrenNames) { + this.groupName = groupName; + this.childrenNames = new LinkedList<>(childrenNames); + } + + public String getGroupName() { + return groupName; + } + + public List getChildrenNames() { + return childrenNames; + } + } + + public static class GroupTreeEntry { + + private final String groupName; + private final List children; + + public GroupTreeEntry(String groupName, List children) { + this.groupName = groupName; + this.children = children; + } + + public String getGroupName() { + return groupName; + } + + public List getChildren() { + return children; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{ " + groupName + " -> [ "); + for (GroupTreeEntry child : children) { + builder.append(child.toString()); + } + builder.append(" ]}"); + + return builder.toString(); + } + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapper.java new file mode 100644 index 0000000000..4150711ac6 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapper.java @@ -0,0 +1,432 @@ +package org.keycloak.federation.ldap.mappers.membership.role; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapper; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.UserModelDelegate; + +/** + * Map realm roles or roles of particular client to LDAP groups + * + * @author Marek Posolda + */ +public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper implements CommonLDAPGroupMapper { + + private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class); + + private final RoleMapperConfig config; + private final RoleLDAPFederationMapperFactory factory; + + public RoleLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm, RoleLDAPFederationMapperFactory factory) { + super(mapperModel, ldapProvider, realm); + this.config = new RoleMapperConfig(mapperModel); + this.factory = factory; + } + + + @Override + public LDAPQuery createLDAPGroupQuery() { + return createRoleQuery(); + } + + @Override + public CommonLDAPGroupMapperConfig getConfig() { + return config; + } + + + @Override + public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) { + LDAPGroupMapperMode mode = config.getMode(); + + // For now, import LDAP role mappings just during create + if (mode == LDAPGroupMapperMode.IMPORT && isCreate) { + + List ldapRoles = getLDAPRoleMappings(ldapUser); + + // Import role mappings from LDAP into Keycloak DB + String roleNameAttr = config.getRoleNameLdapAttribute(); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(roleNameAttr); + + RoleContainerModel roleContainer = getTargetRoleContainer(); + RoleModel role = roleContainer.getRole(roleName); + + if (role == null) { + role = roleContainer.addRole(roleName); + } + + logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername()); + user.grantRole(role); + } + } + } + + @Override + public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) { + } + + + // Sync roles from LDAP to Keycloak DB + @Override + public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() { + UserFederationSyncResult syncResult = new UserFederationSyncResult() { + + @Override + public String getStatus() { + return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated()); + } + + }; + + logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); + + // Send LDAP query + LDAPQuery ldapQuery = createRoleQuery(); + List ldapRoles = ldapQuery.getResultList(); + + RoleContainerModel roleContainer = getTargetRoleContainer(); + String rolesRdnAttr = config.getRoleNameLdapAttribute(); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); + + if (roleContainer.getRole(roleName) == null) { + logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName); + roleContainer.addRole(roleName); + syncResult.increaseAdded(); + } else { + syncResult.increaseUpdated(); + } + } + + return syncResult; + } + + + // Sync roles from Keycloak back to LDAP + @Override + public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() { + UserFederationSyncResult syncResult = new UserFederationSyncResult() { + + @Override + public String getStatus() { + return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated()); + } + + }; + + if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) { + logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString()); + return syncResult; + } + + logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); + + // Send LDAP query to see which roles exists there + LDAPQuery ldapQuery = createRoleQuery(); + List ldapRoles = ldapQuery.getResultList(); + + Set ldapRoleNames = new HashSet<>(); + String rolesRdnAttr = config.getRoleNameLdapAttribute(); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); + ldapRoleNames.add(roleName); + } + + + RoleContainerModel roleContainer = getTargetRoleContainer(); + Set keycloakRoles = roleContainer.getRoles(); + + for (RoleModel keycloakRole : keycloakRoles) { + String roleName = keycloakRole.getName(); + if (ldapRoleNames.contains(roleName)) { + syncResult.increaseUpdated(); + } else { + logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName); + createLDAPRole(roleName); + syncResult.increaseAdded(); + } + } + + return syncResult; + } + + // TODO: Possible to merge with GroupMapper and move to common class + public LDAPQuery createRoleQuery() { + LDAPQuery ldapQuery = new LDAPQuery(ldapProvider); + + // For now, use same search scope, which is configured "globally" and used for user's search. + ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope()); + + String rolesDn = config.getRolesDn(); + ldapQuery.setSearchDn(rolesDn); + + Collection roleObjectClasses = config.getRoleObjectClasses(ldapProvider); + ldapQuery.addObjectClasses(roleObjectClasses); + + String rolesRdnAttr = config.getRoleNameLdapAttribute(); + + String customFilter = config.getCustomLdapFilter(); + if (customFilter != null && customFilter.trim().length() > 0) { + Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter); + ldapQuery.addWhereCondition(customFilterCondition); + } + + String membershipAttr = config.getMembershipLdapAttribute(); + ldapQuery.addReturningLdapAttribute(rolesRdnAttr); + ldapQuery.addReturningLdapAttribute(membershipAttr); + + return ldapQuery; + } + + protected RoleContainerModel getTargetRoleContainer() { + boolean realmRolesMapping = config.isRealmRolesMapping(); + if (realmRolesMapping) { + return realm; + } else { + String clientId = config.getClientId(); + if (clientId == null) { + throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!"); + } + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + throw new ModelException("Can't found requested client with clientId: " + clientId); + } + return client; + } + } + + + public LDAPObject createLDAPRole(String roleName) { + LDAPObject ldapRole = LDAPUtils.createLDAPGroup(ldapProvider, roleName, config.getRoleNameLdapAttribute(), config.getRoleObjectClasses(ldapProvider), + config.getRolesDn(), Collections.>emptyMap()); + + logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, ldapRole.getDn().toString()); + return ldapRole; + } + + public void addRoleMappingInLDAP(String roleName, LDAPObject ldapUser) { + LDAPObject ldapRole = loadLDAPRoleByName(roleName); + if (ldapRole == null) { + ldapRole = createLDAPRole(roleName); + } + + LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true); + } + + public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) { + LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true); + } + + public LDAPObject loadLDAPRoleByName(String roleName) { + LDAPQuery ldapQuery = createRoleQuery(); + Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getRoleNameLdapAttribute(), roleName); + ldapQuery.addWhereCondition(roleNameCondition); + return ldapQuery.getFirstResult(); + } + + protected List getLDAPRoleMappings(LDAPObject ldapUser) { + String strategyKey = config.getUserRolesRetrieveStrategy(); + UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey); + return strategy.getLDAPRoleMappings(this, ldapUser); + } + + @Override + public UserModel proxy(LDAPObject ldapUser, UserModel delegate) { + final LDAPGroupMapperMode mode = config.getMode(); + + // For IMPORT mode, all operations are performed against local DB + if (mode == LDAPGroupMapperMode.IMPORT) { + return delegate; + } else { + return new LDAPRoleMappingsUserDelegate(delegate, ldapUser); + } + } + + @Override + public void beforeLDAPQuery(LDAPQuery query) { + String strategyKey = config.getUserRolesRetrieveStrategy(); + UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey); + strategy.beforeUserLDAPQuery(query); + } + + + + public class LDAPRoleMappingsUserDelegate extends UserModelDelegate { + + private final LDAPObject ldapUser; + private final RoleContainerModel roleContainer; + + // Avoid loading role mappings from LDAP more times per-request + private Set cachedLDAPRoleMappings; + + public LDAPRoleMappingsUserDelegate(UserModel user, LDAPObject ldapUser) { + super(user); + this.ldapUser = ldapUser; + this.roleContainer = getTargetRoleContainer(); + } + + @Override + public Set getRealmRoleMappings() { + if (roleContainer.equals(realm)) { + Set ldapRoleMappings = getLDAPRoleMappingsConverted(); + + if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) { + // Use just role mappings from LDAP + return ldapRoleMappings; + } else { + // Merge mappings from both DB and LDAP + Set modelRoleMappings = super.getRealmRoleMappings(); + ldapRoleMappings.addAll(modelRoleMappings); + return ldapRoleMappings; + } + } else { + return super.getRealmRoleMappings(); + } + } + + @Override + public Set getClientRoleMappings(ClientModel client) { + if (roleContainer.equals(client)) { + Set ldapRoleMappings = getLDAPRoleMappingsConverted(); + + if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) { + // Use just role mappings from LDAP + return ldapRoleMappings; + } else { + // Merge mappings from both DB and LDAP + Set modelRoleMappings = super.getClientRoleMappings(client); + ldapRoleMappings.addAll(modelRoleMappings); + return ldapRoleMappings; + } + } else { + return super.getClientRoleMappings(client); + } + } + + @Override + public boolean hasRole(RoleModel role) { + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); + } + + @Override + public void grantRole(RoleModel role) { + if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) { + + if (role.getContainer().equals(roleContainer)) { + + // We need to create new role mappings in LDAP + cachedLDAPRoleMappings = null; + addRoleMappingInLDAP(role.getName(), ldapUser); + } else { + super.grantRole(role); + } + } else { + super.grantRole(role); + } + } + + @Override + public Set getRoleMappings() { + Set modelRoleMappings = super.getRoleMappings(); + + Set ldapRoleMappings = getLDAPRoleMappingsConverted(); + + if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) { + // For LDAP-only we want to retrieve role mappings of target container just from LDAP + Set modelRolesCopy = new HashSet<>(modelRoleMappings); + for (RoleModel role : modelRolesCopy) { + if (role.getContainer().equals(roleContainer)) { + modelRoleMappings.remove(role); + } + } + } + + modelRoleMappings.addAll(ldapRoleMappings); + return modelRoleMappings; + } + + protected Set getLDAPRoleMappingsConverted() { + if (cachedLDAPRoleMappings != null) { + return new HashSet<>(cachedLDAPRoleMappings); + } + + List ldapRoles = getLDAPRoleMappings(ldapUser); + + Set roles = new HashSet<>(); + String roleNameLdapAttr = config.getRoleNameLdapAttribute(); + for (LDAPObject role : ldapRoles) { + String roleName = role.getAttributeAsString(roleNameLdapAttr); + RoleModel modelRole = roleContainer.getRole(roleName); + if (modelRole == null) { + // Add role to local DB + modelRole = roleContainer.addRole(roleName); + } + roles.add(modelRole); + } + + cachedLDAPRoleMappings = new HashSet<>(roles); + + return roles; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + if (role.getContainer().equals(roleContainer)) { + + LDAPQuery ldapQuery = createRoleQuery(); + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition roleNameCondition = conditionsBuilder.equal(config.getRoleNameLdapAttribute(), role.getName()); + String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute()); + Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr); + ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition); + LDAPObject ldapRole = ldapQuery.getFirstResult(); + + if (ldapRole == null) { + // Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB. + if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) { + super.deleteRoleMapping(role); + } + } else { + // Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error + if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) { + throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY"); + } else { + // Delete ldap role mappings + cachedLDAPRoleMappings = null; + deleteRoleMappingInLDAP(ldapUser, ldapRole); + } + } + } else { + super.deleteRoleMapping(role); + } + } + } + + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java similarity index 60% rename from federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java rename to federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java index 455c1dbf4b..a4ba60f3a8 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java @@ -1,15 +1,25 @@ -package org.keycloak.federation.ldap.mappers; +package org.keycloak.federation.ldap.mappers.membership.role; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; -import org.jboss.logging.Logger; +import org.keycloak.federation.ldap.LDAPConfig; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.MembershipType; +import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy; import org.keycloak.mappers.MapperConfigValidationException; -import org.keycloak.mappers.UserFederationMapper; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; @@ -18,45 +28,49 @@ import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresenta */ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { - private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapperFactory.class); - public static final String PROVIDER_ID = "role-ldap-mapper"; - protected static final List configProperties = new ArrayList(); + protected static final List configProperties = new ArrayList<>(); + protected static final Map userRolesStrategies = new LinkedHashMap<>(); + static { - ProviderConfigProperty rolesDn = createConfigProperty(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", + userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember()); + userRolesStrategies.put(RoleMapperConfig.GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute()); + userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively()); + + ProviderConfigProperty rolesDn = createConfigProperty(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", "LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(rolesDn); - ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute", + ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute", "Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ", - ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN); + ProviderConfigProperty.STRING_TYPE, null); configProperties.add(roleNameLDAPAttribute); - ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes", + ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleMapperConfig.ROLE_OBJECT_CLASSES, "Role Object Classes", "Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(roleObjectClasses); - ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute", + ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute", "Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ", - ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER); + ProviderConfigProperty.STRING_TYPE, null); configProperties.add(membershipLDAPAttribute); List membershipTypes = new LinkedList<>(); - for (RoleLDAPFederationMapper.MembershipType membershipType : RoleLDAPFederationMapper.MembershipType.values()) { + for (MembershipType membershipType : MembershipType.values()) { membershipTypes.add(membershipType.toString()); } - ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", + ProviderConfigProperty membershipType = createConfigProperty(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", "DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " + "UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .", ProviderConfigProperty.LIST_TYPE, membershipTypes); configProperties.add(membershipType); - ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER, + ProviderConfigProperty ldapFilter = createConfigProperty(RoleMapperConfig.ROLES_LDAP_FILTER, "LDAP Filter", "LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'", ProviderConfigProperty.STRING_TYPE, null); @@ -64,10 +78,10 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe List modes = new LinkedList<>(); - for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) { + for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) { modes.add(mode.toString()); } - ProviderConfigProperty mode = createConfigProperty(RoleLDAPFederationMapper.MODE, "Mode", + ProviderConfigProperty mode = createConfigProperty(RoleMapperConfig.MODE, "Mode", "LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " + "retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " + "they are saved to local keycloak DB.", @@ -75,11 +89,8 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe configProperties.add(mode); - List roleRetrievers = new LinkedList<>(); - for (UserRolesRetrieveStrategy retriever : UserRolesRetrieveStrategy.values()) { - roleRetrievers.add(retriever.toString()); - } - ProviderConfigProperty retriever = createConfigProperty(RoleLDAPFederationMapper.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy", + List roleRetrievers = new LinkedList<>(userRolesStrategies.keySet()); + ProviderConfigProperty retriever = createConfigProperty(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy", "Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " + "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " + "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension." @@ -88,11 +99,11 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe configProperties.add(retriever); - ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping", - "If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true"); + ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleMapperConfig.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping", + "If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, null); configProperties.add(useRealmRolesMappings); - ProviderConfigProperty clientIdProperty = createConfigProperty(RoleLDAPFederationMapper.CLIENT_ID, "Client ID", + ProviderConfigProperty clientIdProperty = createConfigProperty(RoleMapperConfig.CLIENT_ID, "Client ID", "Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false", ProviderConfigProperty.CLIENT_LIST_TYPE, null); configProperties.add(clientIdProperty); @@ -118,6 +129,27 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe return configProperties; } + @Override + public Map getDefaultConfig(UserFederationProviderModel providerModel) { + Map defaultValues = new HashMap<>(); + LDAPConfig config = new LDAPConfig(providerModel.getConfig()); + + defaultValues.put(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE, LDAPConstants.CN); + + String roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES; + defaultValues.put(RoleMapperConfig.ROLE_OBJECT_CLASSES, roleObjectClasses); + + defaultValues.put(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, LDAPConstants.MEMBER); + defaultValues.put(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, MembershipType.DN.toString()); + + String mode = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString(); + defaultValues.put(RoleMapperConfig.MODE, mode); + + defaultValues.put(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE); + defaultValues.put(RoleMapperConfig.USE_REALM_ROLES_MAPPING, "true"); + return defaultValues; + } + @Override public String getId() { return PROVIDER_ID; @@ -130,26 +162,30 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe @Override public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { - checkMandatoryConfigAttribute(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", mapperModel); - checkMandatoryConfigAttribute(RoleLDAPFederationMapper.MODE, "Mode", mapperModel); + checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", mapperModel); + checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", mapperModel); - String realmMappings = mapperModel.getConfig().get(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING); + String realmMappings = mapperModel.getConfig().get(RoleMapperConfig.USE_REALM_ROLES_MAPPING); boolean useRealmMappings = Boolean.parseBoolean(realmMappings); if (!useRealmMappings) { - String clientId = mapperModel.getConfig().get(RoleLDAPFederationMapper.CLIENT_ID); + String clientId = mapperModel.getConfig().get(RoleMapperConfig.CLIENT_ID); if (clientId == null || clientId.trim().isEmpty()) { throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used"); } } - String customLdapFilter = mapperModel.getConfig().get(RoleLDAPFederationMapper.ROLES_LDAP_FILTER); + String customLdapFilter = mapperModel.getConfig().get(RoleMapperConfig.ROLES_LDAP_FILTER); if ((customLdapFilter != null && customLdapFilter.trim().length() > 0) && (!customLdapFilter.startsWith("(") || !customLdapFilter.endsWith(")"))) { throw new MapperConfigValidationException("Custom Roles LDAP filter must starts with '(' and ends with ')'"); } } @Override - public UserFederationMapper create(KeycloakSession session) { - return new RoleLDAPFederationMapper(); + protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) { + return new RoleLDAPFederationMapper(mapperModel, federationProvider, realm, this); + } + + protected UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(String strategyKey) { + return userRolesStrategies.get(strategyKey); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleMapperConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleMapperConfig.java new file mode 100644 index 0000000000..a908197c85 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleMapperConfig.java @@ -0,0 +1,97 @@ +package org.keycloak.federation.ldap.mappers.membership.role; + +import java.util.Collection; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig; +import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.UserFederationMapperModel; + +/** + * @author Marek Posolda + */ +public class RoleMapperConfig extends CommonLDAPGroupMapperConfig { + + // LDAP DN where are roles of this tree saved. + public static final String ROLES_DN = "roles.dn"; + + // Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn" + public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute"; + + // Object classes of the role object. + public static final String ROLE_OBJECT_CLASSES = "role.object.classes"; + + // Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID) + public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping"; + + // ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false + public static final String CLIENT_ID = "client.id"; + + // Customized LDAP filter which is added to the whole LDAP query + public static final String ROLES_LDAP_FILTER = "roles.ldap.filter"; + + // See UserRolesRetrieveStrategy + public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE"; + public static final String GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE"; + public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY"; + + + public RoleMapperConfig(UserFederationMapperModel mapperModel) { + super(mapperModel); + } + + public String getRolesDn() { + String rolesDn = mapperModel.getConfig().get(ROLES_DN); + if (rolesDn == null) { + throw new ModelException("Roles DN is null! Check your configuration"); + } + return rolesDn; + } + + @Override + public String getLDAPGroupsDn() { + return getRolesDn(); + } + + public String getRoleNameLdapAttribute() { + String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE); + return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN; + } + + @Override + public String getLDAPGroupNameLdapAttribute() { + return getRoleNameLdapAttribute(); + } + + public Collection getRoleObjectClasses(LDAPFederationProvider ldapProvider) { + String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES); + if (objectClasses == null) { + // For Active directory, the default is 'group' . For other servers 'groupOfNames' + objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES; + } + + return getConfigValues(objectClasses); + } + + public String getCustomLdapFilter() { + return mapperModel.getConfig().get(ROLES_LDAP_FILTER); + } + + public boolean isRealmRolesMapping() { + String realmRolesMapping = mapperModel.getConfig().get(USE_REALM_ROLES_MAPPING); + return realmRolesMapping==null || Boolean.parseBoolean(realmRolesMapping); + } + + public String getClientId() { + return mapperModel.getConfig().get(CLIENT_ID); + } + + + public String getUserRolesRetrieveStrategy() { + String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY); + return strategyString!=null ? strategyString : LOAD_ROLES_BY_MEMBER_ATTRIBUTE; + } + +} diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory index d9e36311cb..9e4a658dd7 100644 --- a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory +++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory @@ -1,3 +1,4 @@ org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory -org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory \ No newline at end of file +org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory +org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory \ No newline at end of file diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/GroupTreeResolverTest.java b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/GroupTreeResolverTest.java new file mode 100644 index 0000000000..853867f5e5 --- /dev/null +++ b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/GroupTreeResolverTest.java @@ -0,0 +1,108 @@ +package org.keycloak.federation.ldap.idm.model; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.federation.ldap.mappers.membership.group.GroupTreeResolver; + +/** + * @author Marek Posolda + */ +public class GroupTreeResolverTest { + + @Test + public void testGroupResolvingCorrect() throws GroupTreeResolver.GroupTreeResolveException { + GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3"); + GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group4", "group5"); + GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group6"); + GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4"); + GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5"); + GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7"); + GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7"); + List groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7); + + GroupTreeResolver resolver = new GroupTreeResolver(); + List groupTree = resolver.resolveGroupTree(groups); + Assert.assertEquals(1, groupTree.size()); + Assert.assertEquals("{ group1 -> [ { group2 -> [ { group4 -> [ ]}{ group5 -> [ ]} ]}{ group3 -> [ { group6 -> [ { group7 -> [ ]} ]} ]} ]}", groupTree.get(0).toString()); + } + + @Test + public void testGroupResolvingCorrect2_multipleRootGroups() throws GroupTreeResolver.GroupTreeResolveException { + GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group8"); + GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2"); + GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2"); + GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5"); + GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group6", "group7"); + GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6"); + GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7"); + GroupTreeResolver.Group group8 = new GroupTreeResolver.Group("group8", "group9"); + GroupTreeResolver.Group group9 = new GroupTreeResolver.Group("group9"); + List groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7, group8, group9); + + GroupTreeResolver resolver = new GroupTreeResolver(); + List groupTree = resolver.resolveGroupTree(groups); + + Assert.assertEquals(2, groupTree.size()); + Assert.assertEquals("{ group3 -> [ { group2 -> [ ]} ]}", groupTree.get(0).toString()); + Assert.assertEquals("{ group4 -> [ { group1 -> [ { group8 -> [ { group9 -> [ ]} ]} ]}{ group5 -> [ { group6 -> [ ]}{ group7 -> [ ]} ]} ]}", groupTree.get(1).toString()); + } + + + @Test + public void testGroupResolvingRecursion() { + GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3"); + GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2"); + GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group4"); + GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group5"); + GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group1"); + GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7"); + GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7"); + List groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7); + + GroupTreeResolver resolver = new GroupTreeResolver(); + try { + resolver.resolveGroupTree(groups); + Assert.fail("Exception expected because of recursion"); + } catch (GroupTreeResolver.GroupTreeResolveException gre) { + Assert.assertTrue(gre.getMessage().startsWith("Recursion detected")); + } + } + + @Test + public void testGroupResolvingMultipleParents() { + GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2"); + GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2"); + GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2"); + GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5"); + GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group4"); + List groups = Arrays.asList(group1, group2, group3, group4, group5); + + GroupTreeResolver resolver = new GroupTreeResolver(); + try { + resolver.resolveGroupTree(groups); + Assert.fail("Exception expected because of some groups have multiple parents"); + } catch (GroupTreeResolver.GroupTreeResolveException gre) { + Assert.assertTrue(gre.getMessage().contains("detected to have multiple parents")); + } + } + + + @Test + public void testGroupResolvingMissingGroup() { + GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2"); + GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group3"); + GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4"); + List groups = Arrays.asList(group1, group2, group4); + + GroupTreeResolver resolver = new GroupTreeResolver(); + try { + resolver.resolveGroupTree(groups); + Assert.fail("Exception expected because of missing referenced group"); + } catch (GroupTreeResolver.GroupTreeResolveException gre) { + Assert.assertEquals("Group 'group3' referenced as member of group 'group2' doesn't exists", gre.getMessage()); + } + } +} diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java index 5631573281..892b0745d6 100644 --- a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java +++ b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java @@ -16,6 +16,7 @@ public class LDAPDnTest { dn.addFirst("uid", "Johny,Depp"); Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString()); + Assert.assertEquals(LDAPDn.fromString("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org"), dn); Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn()); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index cb3aa08df1..e72c8eca3d 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -1046,8 +1046,8 @@ module.controller('UserFederationMapperCtrl', function($scope, realm, provider, function triggerMapperSync(direction) { UserFederationMapperSync.save({ direction: direction, realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) { Notifications.success("Data synced successfully. " + syncResult.status); - }, function() { - Notifications.error("Error during sync of data"); + }, function(error) { + Notifications.error(error.data.errorMessage); }); } @@ -1066,13 +1066,7 @@ module.controller('UserFederationMapperCreateCtrl', function($scope, realm, prov $scope.$watch('mapperType', function() { if ($scope.mapperType != null) { - $scope.mapper.config = {}; - for ( var i = 0; i < $scope.mapperType.properties.length; i++) { - var property = $scope.mapperType.properties[i]; - if (property.type === 'String' || property.type === 'boolean') { - $scope.mapper.config[ property.name ] = property.defaultValue; - } - } + $scope.mapper.config = $scope.mapperType.defaultConfig; } }, true); diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java index 2a03499262..07ec96cc06 100644 --- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java @@ -1,10 +1,14 @@ package org.keycloak.mappers; +import java.util.List; + +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; /** @@ -36,4 +40,9 @@ public interface UserFederationMapper extends Provider { * @param realm */ UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm); + + /** + * Return empty list if doesn't support storing of groups + */ + List getGroupMembers(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm, GroupModel group, int firstResult, int maxResults); } diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java index 7690e8a106..f009c3e2d6 100644 --- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java @@ -1,6 +1,9 @@ package org.keycloak.mappers; +import java.util.Map; + import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation; @@ -36,4 +39,11 @@ public interface UserFederationMapperFactory extends ProviderFactory getDefaultConfig(UserFederationProviderModel providerModel); + } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index e7a33ac6c6..f69d1d5b51 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -5,7 +5,9 @@ import org.jboss.logging.Logger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -166,8 +168,40 @@ public class UserFederationManager implements UserProvider { } @Override - public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { - return session.userStorage().getGroupMembers(realm, group, firstResult, maxResults); + public List getGroupMembers(RealmModel realm, final GroupModel group, int firstResult, int maxResults) { + // Not very effective. For the page X, it is loading also all previous pages 0..X-1 . Improve if needed... + int maxTotal = firstResult + maxResults; + List localMembers = query(new PaginatedQuery() { + + @Override + public List query(RealmModel realm, int first, int max) { + return session.userStorage().getGroupMembers(realm, group, first, max); + } + + }, realm, 0, maxTotal); + + Set result = new LinkedHashSet<>(localMembers); + + for (UserFederationProviderModel federation : realm.getUserFederationProviders()) { + if (result.size() >= maxTotal) { + break; + } + + int max = maxTotal - result.size(); + + UserFederationProvider fed = getFederationProvider(federation); + List current = fed.getGroupMembers(realm, group, 0, max); + if (current != null) { + result.addAll(current); + } + } + + if (result.size() <= firstResult) { + return Collections.emptyList(); + } + + int max = Math.min(maxTotal, result.size()); + return new ArrayList<>(result).subList(firstResult, max); } @Override diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java index 043176e88b..6cd51052c8 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java @@ -104,6 +104,18 @@ public interface UserFederationProvider extends Provider { */ List searchByAttributes(Map attributes, RealmModel realm, int maxResults); + /** + * Return group members from federation storage. Useful if info about group memberships is stored in the federation storage. + * Return empty list if your federation provider doesn't support storing user-group memberships + * + * @param realm + * @param group + * @param firstResult + * @param maxResults + * @return + */ + List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults); + /** * called whenever a Realm is removed * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java index 3c4226a7aa..104627685a 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java @@ -188,6 +188,8 @@ public class UserFederationProviderResource { propRep.setHelpText(prop.getHelpText()); rep.getProperties().add(propRep); } + rep.setDefaultConfig(mapperFactory.getDefaultConfig(this.federationProviderModel)); + types.put(rep.getId(), rep); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java index 51dbbd4f45..02bbf5751b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java @@ -58,6 +58,11 @@ public class DummyUserFederationProvider implements UserFederationProvider { return Collections.emptyList(); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return Collections.emptyList(); + } + @Override public void preRemove(RealmModel realm) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java index 580b3456c9..18db7bb2c3 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java @@ -2,7 +2,10 @@ package org.keycloak.testsuite.federation; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import org.junit.Assert; import org.keycloak.federation.ldap.LDAPFederationProvider; @@ -11,10 +14,15 @@ import org.keycloak.federation.ldap.LDAPUtils; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; -import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper; -import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.role.RoleMapperConfig; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; @@ -22,6 +30,7 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.utils.KeycloakModelUtils; @@ -75,7 +84,7 @@ class FederationTestUtils { if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) { return Arrays.asList(postalCode); } else if ("street".equals(name) && street != null) { - return Arrays.asList(street); + return Collections.singletonList(street); } else { return Collections.emptyList(); } @@ -98,6 +107,9 @@ class FederationTestUtils { Assert.assertEquals(expectedPostalCode, user.getFirstAttribute("postal_code")); } + + // CRUD model mappers + public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) { addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE); } @@ -112,43 +124,72 @@ class FederationTestUtils { return realm.addUserFederationMapper(mapperModel); } - public static void addOrUpdateRoleLDAPMappers(RealmModel realm, UserFederationProviderModel providerModel, RoleLDAPFederationMapper.Mode mode) { + public static void addOrUpdateRoleLDAPMappers(RealmModel realm, UserFederationProviderModel providerModel, LDAPGroupMapperMode mode) { UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper"); if (mapperModel != null) { - mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString()); + mapperModel.getConfig().put(RoleMapperConfig.MODE, mode.toString()); realm.updateUserFederationMapper(mapperModel); } else { String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN); mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID, - RoleLDAPFederationMapper.ROLES_DN, "ou=RealmRoles," + baseDn, - RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "true", - RoleLDAPFederationMapper.MODE, mode.toString()); + RoleMapperConfig.ROLES_DN, "ou=RealmRoles," + baseDn, + RoleMapperConfig.USE_REALM_ROLES_MAPPING, "true", + RoleMapperConfig.MODE, mode.toString()); realm.addUserFederationMapper(mapperModel); } mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper"); if (mapperModel != null) { - mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString()); + mapperModel.getConfig().put(RoleMapperConfig.MODE, mode.toString()); realm.updateUserFederationMapper(mapperModel); } else { String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN); mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID, - RoleLDAPFederationMapper.ROLES_DN, "ou=FinanceRoles," + baseDn, - RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "false", - RoleLDAPFederationMapper.CLIENT_ID, "finance", - RoleLDAPFederationMapper.MODE, mode.toString()); + RoleMapperConfig.ROLES_DN, "ou=FinanceRoles," + baseDn, + RoleMapperConfig.USE_REALM_ROLES_MAPPING, "false", + RoleMapperConfig.CLIENT_ID, "finance", + RoleMapperConfig.MODE, mode.toString()); realm.addUserFederationMapper(mapperModel); } } - public static void syncRolesFromLDAP(RealmModel realm, LDAPFederationProvider ldapProvider, UserFederationProviderModel providerModel) { - RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); + public static void addOrUpdateGroupMapper(RealmModel realm, UserFederationProviderModel providerModel, LDAPGroupMapperMode mode, String descriptionAttrName, String... otherConfigOptions) { + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "groupsMapper"); + if (mapperModel != null) { + mapperModel.getConfig().put(GroupMapperConfig.MODE, mode.toString()); + updateGroupMapperConfigOptions(mapperModel, otherConfigOptions); + realm.updateUserFederationMapper(mapperModel); + } else { + String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN); + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("groupsMapper", providerModel.getId(), GroupLDAPFederationMapperFactory.PROVIDER_ID, + GroupMapperConfig.GROUPS_DN, "ou=Groups," + baseDn, + GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES, descriptionAttrName, + GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true", + GroupMapperConfig.MODE, mode.toString()); + updateGroupMapperConfigOptions(mapperModel, otherConfigOptions); + realm.addUserFederationMapper(mapperModel); + } + } + public static void updateGroupMapperConfigOptions(UserFederationMapperModel mapperModel, String... configOptions) { + for (int i=0 ; i ldapRoles = roleQuery.getResultList(); + for (LDAPObject ldapRole : ldapRoles) { + ldapProvider.getLdapIdentityStore().remove(ldapRole); + } + } + + public static void removeAllLDAPGroups(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) { + UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + LDAPQuery roleQuery = getGroupMapper(mapperModel, ldapProvider, appRealm).createGroupQuery(); List ldapRoles = roleQuery.getResultList(); for (LDAPObject ldapRole : ldapRoles) { ldapProvider.getLdapIdentityStore().remove(ldapRole); @@ -174,6 +225,36 @@ class FederationTestUtils { public static void createLDAPRole(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName, String roleName) { UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName); LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - new RoleLDAPFederationMapper().createLDAPRole(mapperModel, roleName, ldapProvider); + getRoleMapper(mapperModel, ldapProvider, appRealm).createLDAPRole(roleName); + } + + public static LDAPObject createLDAPGroup(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String groupName, String... additionalAttrs) { + UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + Map> additAttrs = new HashMap<>(); + for (int i=0 ; iMarek Posolda + */ +public class LDAPGroupMapper2WaySyncTest { + + @ClassRule + public static LDAPRule ldapRule = new LDAPRule(); + + private static UserFederationProviderModel ldapModel = null; + private static String descriptionAttrName = null; + + @Rule + public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + Map ldapConfig = ldapRule.getConfig(); + ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); + + ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0); + LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description"; + + // Add group mapper + FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + + // Remove all LDAP groups + FederationTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper"); + + // Add some groups for testing into Keycloak + removeAllModelGroups(appRealm); + + GroupModel group1 = appRealm.createGroup("group1"); + appRealm.moveGroup(group1, null); + group1.setSingleAttribute(descriptionAttrName, "group1 - description1"); + + GroupModel group11 = appRealm.createGroup("group11"); + appRealm.moveGroup(group11, group1); + + GroupModel group12 = appRealm.createGroup("group12"); + appRealm.moveGroup(group12, group1); + group12.setSingleAttribute(descriptionAttrName, "group12 - description12"); + + GroupModel group2 = appRealm.createGroup("group2"); + appRealm.moveGroup(group2, null); + } + }); + + + @Test + public void test01_syncNoPreserveGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + // Update group mapper to skip preserve inheritance and check it will pass now + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false"); + realm.updateUserFederationMapper(mapperModel); + + // Sync from Keycloak into LDAP + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + + // Delete all KC groups now + removeAllModelGroups(realm); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2")); + } finally { + keycloakRule.stopSession(session, true); + } + + + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + // Sync from LDAP back into Keycloak + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + + // Assert groups are imported to keycloak. All are at top level + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12"); + GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2"); + + Assert.assertEquals(0, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName)); + + // test drop non-existing works + testDropNonExisting(session, realm, mapperModel, ldapProvider); + } finally { + keycloakRule.stopSession(session, true); + } + } + + @Test + public void test02_syncWithGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + // Update group mapper to skip preserve inheritance and check it will pass now + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true"); + realm.updateUserFederationMapper(mapperModel); + + // Sync from Keycloak into LDAP + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + + // Delete all KC groups now + removeAllModelGroups(realm); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2")); + } finally { + keycloakRule.stopSession(session, true); + } + + + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + // Sync from LDAP back into Keycloak + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + + // Assert groups are imported to keycloak. All are at top level + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"); + GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2"); + + Assert.assertEquals(2, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName)); + + // test drop non-existing works + testDropNonExisting(session, realm, mapperModel, ldapProvider); + } finally { + keycloakRule.stopSession(session, true); + } + } + + + private static void removeAllModelGroups(RealmModel appRealm) { + for (GroupModel group : appRealm.getTopLevelGroups()) { + appRealm.removeGroup(group); + } + } + + private void testDropNonExisting(KeycloakSession session, RealmModel realm, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { + // Put some group directly to LDAP + FederationTestUtils.createLDAPGroup(session, realm, ldapModel, "group3"); + + // Sync and assert our group is still in LDAP + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 0, 4, 0, 0); + Assert.assertNotNull(FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3")); + + // Change config to drop non-existing groups + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true"); + realm.updateUserFederationMapper(mapperModel); + + // Sync and assert group removed from LDAP + syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 0, 4, 1, 0); + Assert.assertNull(FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3")); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java new file mode 100644 index 0000000000..64022fec8b --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java @@ -0,0 +1,224 @@ +package org.keycloak.testsuite.federation; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPFederationProviderFactory; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.MembershipType; +import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPGroupMapperSyncTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static UserFederationProviderModel ldapModel = null; + private static String descriptionAttrName = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + Map ldapConfig = ldapRule.getConfig(); + ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); + + ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0); + LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description"; + + // Add group mapper + FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + + // Add some groups for testing + LDAPObject group1 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description"); + LDAPObject group11 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11"); + LDAPObject group12 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description"); + + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false); + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true); + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Test + public void test01_syncNoPreserveGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm); + + // Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail + LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1"); + LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12"); + LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true); + + try { + new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + Assert.fail("Not expected group sync to pass"); + } catch (ModelException expected) { + Assert.assertTrue(expected.getMessage().contains("Recursion detected")); + } + + // Update group mapper to skip preserve inheritance and check it will pass now + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false"); + realm.updateUserFederationMapper(mapperModel); + + new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + + // Assert groups are imported to keycloak. All are at top level + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12"); + + Assert.assertEquals(0, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName)); + + // Cleanup - remove recursive mapping in LDAP + LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true); + + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02_syncWithGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm); + + // Sync groups with inheritance + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0); + + // Assert groups are imported to keycloak including their inheritance from LDAP + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group12")); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"); + + Assert.assertEquals(2, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName)); + + // Update description attributes in LDAP + LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1"); + group1.setSingleAttribute(descriptionAttrName, "group1 - changed description"); + ldapProvider.getLdapIdentityStore().update(group1); + + LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12"); + group12.setAttribute(descriptionAttrName, null); + ldapProvider.getLdapIdentityStore().update(group12); + + // Sync and assert groups updated + syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0); + + // Assert attributes changed in keycloak + kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"); + Assert.assertEquals("group1 - changed description", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup12.getFirstAttribute(descriptionAttrName)); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test03_syncWithDropNonExistingGroups() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + // Sync groups with inheritance + UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0); + + // Assert groups are imported to keycloak including their inheritance from LDAP + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12")); + + Assert.assertEquals(2, kcGroup1.getSubGroups().size()); + + // Create some new groups in keycloak + GroupModel model1 = realm.createGroup("model1"); + realm.moveGroup(model1, null); + GroupModel model2 = realm.createGroup("model2"); + kcGroup1.addChild(model2); + + // Sync groups again from LDAP. Nothing deleted + syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + FederationTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0); + + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/model1")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2")); + + // Update group mapper to drop non-existing groups during sync + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true"); + realm.updateUserFederationMapper(mapperModel); + + // Sync groups again from LDAP. Assert LDAP non-existing groups deleted + syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + Assert.assertEquals(3, syncResult.getUpdated()); + Assert.assertTrue(syncResult.getRemoved() >= 2); + + // Sync and assert groups updated + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/model1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2")); + } finally { + keycloakRule.stopSession(session, false); + } + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java new file mode 100644 index 0000000000..2469f455ee --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java @@ -0,0 +1,291 @@ +package org.keycloak.testsuite.federation; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPFederationProviderFactory; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.MembershipType; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapper; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPGroupMapperTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static UserFederationProviderModel ldapModel = null; + private static String descriptionAttrName = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app"); + FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "john", "john@test.com", "password-app"); + + Map ldapConfig = ldapRule.getConfig(); + ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); + + ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0); + LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description"; + + // Add group mapper + FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + + // Add some groups for testing + LDAPObject group1 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description"); + LDAPObject group11 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11"); + LDAPObject group12 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description"); + + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false); + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true); + + // Sync LDAP groups to Keycloak DB + UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapFedProvider, session, appRealm); + + // Delete all LDAP users + FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); + + // Add some LDAP users for testing + LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); + ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); + + LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678"); + ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1"); + + LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910"); + ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1"); + + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Test + public void test01_ldapOnlyGroupMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString()); + appRealm.updateUserFederationMapper(mapperModel); + + UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + // 1 - Grant some groups in LDAP + + // This group should already exists as it was imported from LDAP + GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); + john.joinGroup(group1); + + // This group should already exists as it was imported from LDAP + GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); + mary.joinGroup(group11); + + // This group should already exists as it was imported from LDAP + GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); + john.joinGroup(group12); + mary.joinGroup(group12); + + // 2 - Check that group mappings are not in local Keycloak DB (They are in LDAP). + + UserModel johnDb = session.userStorage().getUserByUsername("johnkeycloak", appRealm); + Set johnDbGroups = johnDb.getGroups(); + Assert.assertEquals(0, johnDbGroups.size()); + + // 3 - Check that group mappings are in LDAP and hence available through federation + + Set johnGroups = john.getGroups(); + Assert.assertEquals(2, johnGroups.size()); + Assert.assertTrue(johnGroups.contains(group1)); + Assert.assertFalse(johnGroups.contains(group11)); + Assert.assertTrue(johnGroups.contains(group12)); + + // 4 - Check through userProvider + List group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10); + List group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10); + List group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10); + + Assert.assertEquals(1, group1Members.size()); + Assert.assertEquals("johnkeycloak", group1Members.get(0).getUsername()); + Assert.assertEquals(1, group11Members.size()); + Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername()); + Assert.assertEquals(2, group12Members.size()); + + // 4 - Delete some group mappings and check they are deleted + + john.leaveGroup(group1); + john.leaveGroup(group12); + + mary.leaveGroup(group1); + mary.leaveGroup(group12); + + johnGroups = john.getGroups(); + Assert.assertEquals(0, johnGroups.size()); + + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02_readOnlyGroupMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.READ_ONLY.toString()); + appRealm.updateUserFederationMapper(mapperModel); + + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); + GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); + GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); + + // Add some group mappings directly into LDAP + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm); + + LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak"); + groupMapper.addGroupMappingInLDAP("group1", maryLdap); + groupMapper.addGroupMappingInLDAP("group11", maryLdap); + + // Add some group mapping to model + mary.joinGroup(group12); + + // Assert that mary has both LDAP and DB mapped groups + Set maryGroups = mary.getGroups(); + Assert.assertEquals(3, maryGroups.size()); + Assert.assertTrue(maryGroups.contains(group1)); + Assert.assertTrue(maryGroups.contains(group11)); + Assert.assertTrue(maryGroups.contains(group12)); + + // Assert that access through DB will have just DB mapped groups + UserModel maryDB = session.userStorage().getUserByUsername("marykeycloak", appRealm); + Set maryDBGroups = maryDB.getGroups(); + Assert.assertFalse(maryDBGroups.contains(group1)); + Assert.assertFalse(maryDBGroups.contains(group11)); + Assert.assertTrue(maryDBGroups.contains(group12)); + + // Check through userProvider + List group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10); + List group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10); + List group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10); + Assert.assertEquals(1, group1Members.size()); + Assert.assertEquals("marykeycloak", group1Members.get(0).getUsername()); + Assert.assertEquals(1, group11Members.size()); + Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername()); + Assert.assertEquals(1, group12Members.size()); + Assert.assertEquals("marykeycloak", group12Members.get(0).getUsername()); + + mary.leaveGroup(group12); + try { + mary.leaveGroup(group1); + Assert.fail("It wasn't expected to successfully delete LDAP group mappings in READ_ONLY mode"); + } catch (ModelException expected) { + } + + // Delete role mappings directly in LDAP + deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group1"); + deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group11"); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test03_importGroupMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper"); + FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.IMPORT.toString()); + appRealm.updateUserFederationMapper(mapperModel); + + // Add some group mappings directly in LDAP + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm); + + LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak"); + groupMapper.addGroupMappingInLDAP("group11", robLdap); + groupMapper.addGroupMappingInLDAP("group12", robLdap); + + // Get user and check that he has requested groupa from LDAP + UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm); + Set robGroups = rob.getGroups(); + + GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); + GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); + GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); + + Assert.assertFalse(robGroups.contains(group1)); + Assert.assertTrue(robGroups.contains(group11)); + Assert.assertTrue(robGroups.contains(group12)); + + // Delete some group mappings in LDAP and check that it doesn't have any effect and user still has groups + deleteGroupMappingsInLDAP(groupMapper, robLdap, "group11"); + deleteGroupMappingsInLDAP(groupMapper, robLdap, "group12"); + robGroups = rob.getGroups(); + Assert.assertTrue(robGroups.contains(group11)); + Assert.assertTrue(robGroups.contains(group12)); + + // Delete group mappings through model and verifies that user doesn't have them anymore + rob.leaveGroup(group11); + rob.leaveGroup(group12); + robGroups = rob.getGroups(); + Assert.assertEquals(0, robGroups.size()); + } finally { + keycloakRule.stopSession(session, false); + } + } + + private void deleteGroupMappingsInLDAP(GroupLDAPFederationMapper groupMapper, LDAPObject ldapUser, String groupName) { + LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName(groupName); + groupMapper.deleteGroupMappingInLDAP(ldapUser, ldapGroup); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java index a94618cc2c..8bdc093b07 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java @@ -117,7 +117,6 @@ public class LDAPMultipleAttributesTest { KeycloakSession session = keycloakRule.startSession(); try { RealmModel appRealm = session.realms().getRealmByName("test"); - LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); FederationTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441"); @@ -155,7 +154,7 @@ public class LDAPMultipleAttributesTest { } private void assertPostalCodes(List postalCodes, String... expectedPostalCodes) { - if (expectedPostalCodes == null && postalCodes.isEmpty()) { + if (expectedPostalCodes == null || postalCodes.isEmpty()) { return; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java index 2d6eac90dc..565694f840 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java @@ -14,10 +14,8 @@ import org.junit.runners.MethodSorters; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.federation.ldap.idm.model.LDAPObject; -import org.keycloak.federation.ldap.idm.query.Condition; -import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; -import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; -import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapper; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -70,7 +68,7 @@ public class LDAPRoleMappingsTest { ClientModel finance = appRealm.addClient("finance"); // Delete all LDAP roles - FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.LDAP_ONLY); + FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY); FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "realmRolesMapper"); FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper"); @@ -120,7 +118,7 @@ public class LDAPRoleMappingsTest { try { RealmModel appRealm = session.realms().getRealmByName("test"); - FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.LDAP_ONLY); + FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY); UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); @@ -212,7 +210,7 @@ public class LDAPRoleMappingsTest { try { RealmModel appRealm = session.realms().getRealmByName("test"); - FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.READ_ONLY); + FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.READ_ONLY); UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); @@ -224,12 +222,13 @@ public class LDAPRoleMappingsTest { } // Add some role mappings directly into LDAP - RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); UserFederationMapperModel roleMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "realmRolesMapper"); LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + RoleLDAPFederationMapper roleMapper = FederationTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm); + LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak"); - roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, maryLdap); - roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, maryLdap); + roleMapper.addRoleMappingInLDAP("realmRole1", maryLdap); + roleMapper.addRoleMappingInLDAP("realmRole2", maryLdap); // Add some role to model mary.grantRole(realmRole3); @@ -255,8 +254,8 @@ public class LDAPRoleMappingsTest { } // Delete role mappings directly in LDAP - deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole1"); - deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole2"); + deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole1"); + deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole2"); } finally { keycloakRule.stopSession(session, false); } @@ -282,15 +281,16 @@ public class LDAPRoleMappingsTest { try { RealmModel appRealm = session.realms().getRealmByName("test"); - FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.IMPORT); + FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.IMPORT); // Add some role mappings directly in LDAP - RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); UserFederationMapperModel roleMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "realmRolesMapper"); LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + RoleLDAPFederationMapper roleMapper = FederationTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm); + LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak"); - roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, robLdap); - roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, robLdap); + roleMapper.addRoleMappingInLDAP("realmRole1", robLdap); + roleMapper.addRoleMappingInLDAP("realmRole2", robLdap); // Get user and check that he has requested roles from LDAP UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm); @@ -311,8 +311,8 @@ public class LDAPRoleMappingsTest { Assert.assertTrue(robRoles.contains(realmRole3)); // Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role - deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole1"); - deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole2"); + deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole1"); + deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole2"); robRoles = rob.getRealmRoleMappings(); Assert.assertTrue(robRoles.contains(realmRole1)); Assert.assertTrue(robRoles.contains(realmRole2)); @@ -330,12 +330,8 @@ public class LDAPRoleMappingsTest { } } - private void deleteRoleMappingsInLDAP(UserFederationMapperModel roleMapperModel, RoleLDAPFederationMapper roleMapper, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String roleName) { - LDAPQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider); - LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); - Condition roleNameCondition = conditionsBuilder.equal(LDAPConstants.CN, roleName); - ldapQuery.addWhereCondition(roleNameCondition); - LDAPObject ldapRole1 = ldapQuery.getFirstResult(); - roleMapper.deleteRoleMappingInLDAP(roleMapperModel, ldapProvider, ldapUser, ldapRole1); + private void deleteRoleMappingsInLDAP(RoleLDAPFederationMapper roleMapper, LDAPObject ldapUser, String roleName) { + LDAPObject ldapRole1 = roleMapper.loadLDAPRoleByName(roleName); + roleMapper.deleteRoleMappingInLDAP(ldapUser, ldapRole1); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index f4a7c6f1d8..76ed256b21 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -93,7 +93,7 @@ public class SyncProvidersTest { try { KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); UserFederationSyncResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel); - assertSyncEquals(syncResult, 5, 0, 0, 0); + FederationTestUtils.assertSyncEquals(syncResult, 5, 0, 0, 0); } finally { keycloakRule.stopSession(session, false); } @@ -139,7 +139,7 @@ public class SyncProvidersTest { // Trigger partial sync KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); UserFederationSyncResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel); - assertSyncEquals(syncResult, 1, 1, 0, 0); + FederationTestUtils.assertSyncEquals(syncResult, 1, 1, 0, 0); } finally { keycloakRule.stopSession(session, false); } @@ -274,7 +274,7 @@ public class SyncProvidersTest { FederationTestUtils.assertUserImported(session.users(), testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121"); FederationTestUtils.assertUserImported(session.users(), testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122"); UserModel user1 = session.users().getUserByUsername("user1", testRealm); - Assert.assertEquals("user1", user1.getFirstAttribute(LDAPConstants.LDAP_ID)); + Assert.assertEquals("user1", user1.getFirstAttribute(LDAPConstants.LDAP_ID)); // Revert config changes UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderByDisplayName(ldapModel.getDisplayName(), testRealm); @@ -385,11 +385,4 @@ public class SyncProvidersTest { throw new RuntimeException(ie); } } - - private void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) { - Assert.assertEquals(expectedAdded, syncResult.getAdded()); - Assert.assertEquals(expectedUpdated, syncResult.getUpdated()); - Assert.assertEquals(expectedRemoved, syncResult.getRemoved()); - Assert.assertEquals(expectedFailed, syncResult.getFailed()); - } } diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif index 176e19b81a..4df8b5e61a 100644 --- a/testsuite/integration/src/test/resources/ldap/users.ldif +++ b/testsuite/integration/src/test/resources/ldap/users.ldif @@ -18,3 +18,8 @@ dn: ou=FinanceRoles,dc=keycloak,dc=org objectclass: top objectclass: organizationalUnit ou: FinanceRoles + +dn: ou=Groups,dc=keycloak,dc=org +objectclass: top +objectclass: organizationalUnit +ou: Groups From 20548b402d6455704c4d31e9791073d89dfcec76 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 22 Dec 2015 09:35:53 +0100 Subject: [PATCH 25/65] Separate package for LDAP tests --- .../ldap/LDAPFederationProviderFactory.java | 25 +++++++++++++++++++ .../group/GroupLDAPFederationMapper.java | 10 ++++---- .../{ => ldap}/FederationTestUtils.java | 4 +-- .../{ => ldap}/LDAPExampleServlet.java | 2 +- .../{ => ldap}/LDAPTestConfiguration.java | 2 +- .../FederationProvidersIntegrationTest.java | 3 ++- .../base}/LDAPGroupMapper2WaySyncTest.java | 11 ++------ .../base}/LDAPGroupMapperSyncTest.java | 3 ++- .../{ => ldap/base}/LDAPGroupMapperTest.java | 8 ++---- .../base}/LDAPMultipleAttributesTest.java | 6 +++-- .../{ => ldap/base}/LDAPRoleMappingsTest.java | 3 ++- .../{ => ldap/base}/SyncProvidersTest.java | 3 ++- .../keycloak/testsuite/rule/KerberosRule.java | 2 +- .../org/keycloak/testsuite/rule/LDAPRule.java | 2 +- 14 files changed, 52 insertions(+), 32 deletions(-) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap}/FederationTestUtils.java (99%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap}/LDAPExampleServlet.java (97%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap}/LDAPTestConfiguration.java (99%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/FederationProvidersIntegrationTest.java (99%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/LDAPGroupMapper2WaySyncTest.java (96%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/LDAPGroupMapperSyncTest.java (99%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/LDAPGroupMapperTest.java (98%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/LDAPMultipleAttributesTest.java (97%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/LDAPRoleMappingsTest.java (99%) rename testsuite/integration/src/test/java/org/keycloak/testsuite/federation/{ => ldap/base}/SyncProvidersTest.java (99%) diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 31ff560475..551d940acd 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -16,6 +16,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory; +import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; @@ -192,6 +193,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi @Override public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) { + syncMappers(sessionFactory, realmId, model); + logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getDisplayName()); LDAPQuery userQuery = createQuery(sessionFactory, realmId, model); @@ -205,6 +208,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi @Override public UserFederationSyncResult syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) { + syncMappers(sessionFactory, realmId, model); + logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getDisplayName()); // Sync newly created and updated users @@ -221,6 +226,26 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi return result; } + protected void syncMappers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + LDAPFederationProvider ldapProvider = getInstance(session, model); + RealmModel realm = session.realms().getRealm(realmId); + Set mappers = realm.getUserFederationMappersByFederationProvider(model.getId()); + for (UserFederationMapperModel mapperModel : mappers) { + UserFederationMapper ldapMapper = session.getProvider(UserFederationMapper.class, mapperModel.getFederationMapperType()); + UserFederationSyncResult syncResult = ldapMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + if (syncResult.getAdded() > 0 || syncResult.getUpdated() > 0 || syncResult.getRemoved() > 0 || syncResult.getFailed() > 0) { + logger.infof("Sync of federation mapper '%s' finished. Status: %s", mapperModel.getName(), syncResult.toString()); + } + } + } + + }); + } + protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) { final UserFederationSyncResult syncResult = new UserFederationSyncResult(); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java index 5115e9ae17..1d2a407604 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java @@ -237,17 +237,17 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl } if (kcGroup != null) { - logger.infof("Updated Keycloak group '%s' from LDAP", kcGroup.getName()); + logger.debugf("Updated Keycloak group '%s' from LDAP", kcGroup.getName()); updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName())); syncResult.increaseUpdated(); } else { kcGroup = realm.createGroup(groupTreeEntry.getGroupName()); if (kcParent == null) { realm.moveGroup(kcGroup, null); - logger.infof("Imported top-level group '%s' from LDAP", kcGroup.getName()); + logger.debugf("Imported top-level group '%s' from LDAP", kcGroup.getName()); } else { realm.moveGroup(kcGroup, kcParent); - logger.infof("Imported group '%s' from LDAP as child of group '%s'", kcGroup.getName(), kcParent.getName()); + logger.debugf("Imported group '%s' from LDAP as child of group '%s'", kcGroup.getName(), kcParent.getName()); } updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName())); @@ -266,7 +266,7 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl List allGroups = realm.getGroups(); for (GroupModel kcGroup : allGroups) { if (!visitedGroupIds.contains(kcGroup.getId())) { - logger.infof("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName()); + logger.debugf("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName()); realm.removeGroup(kcGroup); syncResult.increaseRemoved(); } @@ -533,7 +533,7 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, user); if (kcGroup != null) { - logger.infof("User [%s] joins group [%s] during import from LDAP", user.getUsername(), kcGroup.getName()); + logger.debugf("User '%s' joins group '%s' during import from LDAP", user.getUsername(), kcGroup.getName()); user.joinGroup(kcGroup); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java index 18db7bb2c3..f189e2012c 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap; import java.util.Arrays; import java.util.Collections; @@ -40,7 +40,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; /** * @author Marek Posolda */ -class FederationTestUtils { +public class FederationTestUtils { public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) { UserModel user = session.userStorage().addUser(realm, username); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPExampleServlet.java similarity index 97% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPExampleServlet.java index 80bc9f0e84..dfdeb05e05 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPExampleServlet.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap; import java.io.IOException; import java.io.PrintWriter; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java index c3e3542885..46b1be4d61 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap; import java.io.File; import java.io.InputStream; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java index 7a8a01ab0d..5702a451a5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; import org.junit.Assert; import org.junit.ClassRule; @@ -30,6 +30,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.AccessToken; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; import org.keycloak.testsuite.pages.AccountPasswordPage; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AppPage; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapper2WaySyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java similarity index 96% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapper2WaySyncTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java index ee45417e25..25234d25e2 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapper2WaySyncTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java @@ -1,27 +1,19 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; -import java.util.List; import java.util.Map; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; -import org.junit.rules.TestRule; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; -import org.keycloak.federation.ldap.LDAPUtils; -import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; -import org.keycloak.federation.ldap.mappers.membership.MembershipType; -import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; -import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; @@ -29,6 +21,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java index 64022fec8b..33cb2840a4 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperSyncTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; import java.util.Map; @@ -29,6 +29,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java similarity index 98% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java index 2469f455ee..4aaac4148b 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPGroupMapperTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; import java.util.List; import java.util.Map; @@ -20,22 +20,18 @@ import org.keycloak.federation.ldap.mappers.membership.MembershipType; import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig; -import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapper; -import org.keycloak.models.AccountRoles; -import org.keycloak.models.ClientModel; -import org.keycloak.models.Constants; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java similarity index 97% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java index 8bdc093b07..6bca1ee066 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; import java.net.URL; import java.util.Arrays; @@ -32,6 +32,8 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; +import org.keycloak.testsuite.federation.ldap.LDAPExampleServlet; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; @@ -154,7 +156,7 @@ public class LDAPMultipleAttributesTest { } private void assertPostalCodes(List postalCodes, String... expectedPostalCodes) { - if (expectedPostalCodes == null || postalCodes.isEmpty()) { + if (expectedPostalCodes == null && postalCodes.isEmpty()) { return; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java index 565694f840..50cc6d2590 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; import java.util.Map; import java.util.Set; @@ -30,6 +30,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java index 76ed256b21..39ef19e5c6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.federation; +package org.keycloak.testsuite.federation.ldap.base; import org.junit.Assert; import org.junit.ClassRule; @@ -23,6 +23,7 @@ import org.keycloak.models.UserProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.UsersSyncManager; +import org.keycloak.testsuite.federation.ldap.FederationTestUtils; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; import org.keycloak.testsuite.DummyUserFederationProviderFactory; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java index 57bf79ac29..eb01f9bd6f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java @@ -5,7 +5,7 @@ import java.net.URL; import java.util.Properties; import org.jboss.logging.Logger; -import org.keycloak.testsuite.federation.LDAPTestConfiguration; +import org.keycloak.testsuite.federation.ldap.LDAPTestConfiguration; import org.keycloak.util.ldap.KerberosEmbeddedServer; import org.keycloak.util.ldap.LDAPEmbeddedServer; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java index 438938f046..b86df2e317 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java @@ -4,7 +4,7 @@ import java.util.Map; import java.util.Properties; import org.junit.rules.ExternalResource; -import org.keycloak.testsuite.federation.LDAPTestConfiguration; +import org.keycloak.testsuite.federation.ldap.LDAPTestConfiguration; import org.keycloak.util.ldap.LDAPEmbeddedServer; /** From 0c293089c3c124b85a7ab0b3a89f55a75b607cff Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 22 Dec 2015 12:29:17 +0100 Subject: [PATCH 26/65] KEYCLOAK-2154 Group mapper fixes --- .../ldap/LDAPFederationProvider.java | 22 +-- .../mappers/AbstractLDAPFederationMapper.java | 8 ++ .../mappers/membership/MembershipType.java | 127 +++++++++++++++++- .../group/GroupLDAPFederationMapper.java | 40 +----- .../GroupLDAPFederationMapperFactory.java | 8 ++ .../ldap/base/LDAPGroupMapperSyncTest.java | 3 + .../ldap/base/LDAPGroupMapperTest.java | 3 + 7 files changed, 154 insertions(+), 57 deletions(-) diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 714a97a16b..698a392c12 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -206,25 +206,9 @@ public class LDAPFederationProvider implements UserFederationProvider { return Collections.emptyList(); } - public List loadUsersByLDAPDns(Collection userDns, RealmModel realm) { - // We have dns of users, who are members of our group. Load them now - LDAPQuery query = LDAPUtils.createQueryForUserSearch(this, realm); - LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); - Condition[] orSubconditions = new Condition[userDns.size()]; - int index = 0; - for (LDAPDn userDn : userDns) { - Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue()); - orSubconditions[index] = condition; - index++; - } - Condition orCondition = conditionsBuilder.orCondition(orSubconditions); - query.addWhereCondition(orCondition); - List ldapUsers = query.getResultList(); - - // We have ldapUsers, Need to load users from KC DB or import them here - List result = new LinkedList<>(); - for (LDAPObject ldapUser : ldapUsers) { - String username = LDAPUtils.getUsername(ldapUser, getLdapIdentityStore().getConfig()); + public List loadUsersByUsernames(List usernames, RealmModel realm) { + List result = new ArrayList<>(); + for (String username : usernames) { UserModel kcUser = session.users().getUserByUsername(username, realm); if (!model.getId().equals(kcUser.getFederationLink())) { logger.warnf("Incorrect federation provider of user %s" + kcUser.getUsername()); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java index 9b08f5fe3b..2a79a48673 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java @@ -75,4 +75,12 @@ public abstract class AbstractLDAPFederationMapper { String paramm = mapperModel.getConfig().get(paramName); return Boolean.parseBoolean(paramm); } + + public LDAPFederationProvider getLdapProvider() { + return ldapProvider; + } + + public RealmModel getRealm() { + return realm; + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java index 624ed3b07d..cee0c73e15 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/MembershipType.java @@ -1,5 +1,24 @@ package org.keycloak.federation.ldap.mappers.membership; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.keycloak.federation.ldap.LDAPConfig; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPDn; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + /** * @author Marek Posolda */ @@ -8,10 +27,114 @@ public enum MembershipType { /** * Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" ) */ - DN, + DN { + + @Override + public Set getLDAPSubgroups(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup) { + CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); + return getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), LDAPDn.fromString(config.getLDAPGroupsDn())); + } + + // Get just those members of specified group, which are descendants of "requiredParentDn" + protected Set getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) { + Set allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup); + + // Filter and keep just groups + Set result = new HashSet<>(); + for (String membership : allMemberships) { + LDAPDn childDn = LDAPDn.fromString(membership); + if (childDn.isDescendantOf(requiredParentDn)) { + result.add(childDn); + } + } + return result; + } + + @Override + public List getGroupMembers(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) { + RealmModel realm = groupMapper.getRealm(); + LDAPFederationProvider ldapProvider = groupMapper.getLdapProvider(); + CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); + + LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); + Set userDns = getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), usersDn); + + if (userDns == null) { + return Collections.emptyList(); + } + + if (userDns.size() <= firstResult) { + return Collections.emptyList(); + } + + List dns = new ArrayList<>(userDns); + int max = Math.min(dns.size(), firstResult + maxResults); + dns = dns.subList(firstResult, max); + + // If usernameAttrName is same like DN, we can just retrieve usernames from DNs + List usernames = new LinkedList<>(); + LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig(); + if (ldapConfig.getUsernameLdapAttribute().equals(ldapConfig.getRdnLdapAttribute())) { + for (LDAPDn userDn : dns) { + String username = userDn.getFirstRdnAttrValue(); + usernames.add(username); + } + } else { + LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm); + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition[] orSubconditions = new Condition[dns.size()]; + int index = 0; + for (LDAPDn userDn : dns) { + Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue()); + orSubconditions[index] = condition; + index++; + } + Condition orCondition = conditionsBuilder.orCondition(orSubconditions); + query.addWhereCondition(orCondition); + List ldapUsers = query.getResultList(); + for (LDAPObject ldapUser : ldapUsers) { + String username = LDAPUtils.getUsername(ldapUser, ldapConfig); + usernames.add(username); + } + } + + // We have dns of users, who are members of our group. Load them now + return ldapProvider.loadUsersByUsernames(usernames, realm); + } + + }, + /** * Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" ) */ - UID + UID { + + // Group inheritance not supported for this config + @Override + public Set getLDAPSubgroups(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup) { + return Collections.emptySet(); + } + + @Override + public List getGroupMembers(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) { + String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute(); + Set memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup); + + if (memberUids == null || memberUids.size() <= firstResult) { + return Collections.emptyList(); + } + + List uids = new ArrayList<>(memberUids); + int max = Math.min(memberUids.size(), firstResult + maxResults); + uids = uids.subList(firstResult, max); + + return groupMapper.getLdapProvider().loadUsersByUsernames(uids, groupMapper.getRealm()); + } + + }; + + public abstract Set getLDAPSubgroups(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup); + + public abstract List getGroupMembers(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java index 1d2a407604..911e2920e9 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java @@ -1,6 +1,5 @@ package org.keycloak.federation.ldap.mappers.membership.group; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -27,8 +26,6 @@ import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy import org.keycloak.models.GroupModel; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleContainerModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserModel; @@ -115,22 +112,8 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl } protected Set getLDAPSubgroups(LDAPObject ldapGroup) { - return getLDAPMembersWithParent(ldapGroup, LDAPDn.fromString(config.getGroupsDn())); - } - - // Get just those members of specified group, which are descendants of "requiredParentDn" - protected Set getLDAPMembersWithParent(LDAPObject ldapGroup, LDAPDn requiredParentDn) { - Set allMemberships = LDAPUtils.getExistingMemberships(config.getMembershipLdapAttribute(), ldapGroup); - - // Filter and keep just groups - Set result = new HashSet<>(); - for (String membership : allMemberships) { - LDAPDn childDn = LDAPDn.fromString(membership); - if (childDn.isDescendantOf(requiredParentDn)) { - result.add(childDn); - } - } - return result; + MembershipType membershipType = config.getMembershipTypeLdapAttribute(); + return membershipType.getLDAPSubgroups(this, ldapGroup); } @@ -461,23 +444,8 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl return Collections.emptyList(); } - LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); - Set userDns = getLDAPMembersWithParent(ldapGroup, usersDn); - - if (userDns == null) { - return Collections.emptyList(); - } - - if (userDns.size() <= firstResult) { - return Collections.emptyList(); - } - - List dns = new ArrayList<>(userDns); - int max = Math.min(dns.size(), firstResult + maxResults); - dns = dns.subList(firstResult, max); - - // We have dns of users, who are members of our group. Load them now - return ldapProvider.loadUsersByLDAPDns(dns, realm); + MembershipType membershipType = config.getMembershipTypeLdapAttribute(); + return membershipType.getGroupMembers(this, ldapGroup, firstResult, maxResults); } public void addGroupMappingInLDAP(String groupName, LDAPObject ldapUser) { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java index 1e06fa995b..464cc0d92e 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java @@ -11,6 +11,7 @@ import org.keycloak.federation.ldap.LDAPConfig; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig; import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode; import org.keycloak.federation.ldap.mappers.membership.MembershipType; import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy; @@ -170,6 +171,13 @@ public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapp public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", mapperModel); checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", mapperModel); + + String mt = mapperModel.getConfig().get(CommonLDAPGroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE); + MembershipType membershipType = mt==null ? MembershipType.DN : Enum.valueOf(MembershipType.class, mt); + boolean preserveGroupInheritance = Boolean.parseBoolean(mapperModel.getConfig().get(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE)); + if (preserveGroupInheritance && membershipType != MembershipType.DN) { + throw new MapperConfigValidationException("Not possible to preserve group inheritance and use UID membership type together"); + } } @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java index 33cb2840a4..f7e17f6312 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java @@ -59,6 +59,9 @@ public class LDAPGroupMapperSyncTest { // Add group mapper FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + // Remove all LDAP groups + FederationTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper"); + // Add some groups for testing LDAPObject group1 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description"); LDAPObject group11 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java index 4aaac4148b..8778735291 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java @@ -64,6 +64,9 @@ public class LDAPGroupMapperTest { // Add group mapper FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + // Remove all LDAP groups + FederationTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper"); + // Add some groups for testing LDAPObject group1 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description"); LDAPObject group11 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11"); From 9172b5472efd35b4e7bef9015e64f9fe563b8740 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 22 Dec 2015 12:53:19 -0200 Subject: [PATCH 27/65] [KEYCLOAK-2202] - Removing LoginProtocol in order to reuse SAML settings. --- .../keycloak/protocol/saml/SamlService.java | 39 +++---- .../ecp/SamlEcpProfileProtocolFactory.java | 109 ------------------ .../profile/ecp/SamlEcpProfileService.java | 74 +++++++++++- ...org.keycloak.protocol.LoginProtocolFactory | 3 +- .../AuthenticationProcessor.java | 4 +- .../protocol/AuthorizationEndpointBase.java | 2 +- .../managers/AuthenticationManager.java | 19 ++- 7 files changed, 106 insertions(+), 144 deletions(-) delete mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index f9aa30b148..67edd49c8e 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -35,7 +35,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.AuthorizationEndpointBase; -import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService; import org.keycloak.saml.SAML2LogoutResponseBuilder; @@ -224,7 +223,7 @@ public class SamlService extends AuthorizationEndpointBase { } ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(getLoginProtocol()); + clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirect); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); @@ -373,7 +372,7 @@ public class SamlService extends AuthorizationEndpointBase { } } - public class PostBindingProtocol extends BindingProtocol { + protected class PostBindingProtocol extends BindingProtocol { @Override protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException { @@ -446,12 +445,12 @@ public class SamlService extends AuthorizationEndpointBase { } protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) { - LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); - protocol.setRealm(realm) - .setHttpHeaders(request.getHttpHeaders()) - .setUriInfo(uriInfo) - .setEventBuilder(event); - return handleBrowserAuthenticationRequest(clientSession, protocol, isPassive); + SamlProtocol samlProtocol = new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo); + return newBrowserAuthentication(clientSession, isPassive, samlProtocol); + } + + protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, SamlProtocol samlProtocol) { + return handleBrowserAuthenticationRequest(clientSession, samlProtocol, isPassive); } /** @@ -471,16 +470,6 @@ public class SamlService extends AuthorizationEndpointBase { return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState); } - @POST - @Consumes("application/soap+xml") - public Response soapBinding(InputStream inputStream) { - SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, authManager); - - ResteasyProviderFactory.getInstance().injectProperties(bindingService); - - return bindingService.authenticate(inputStream); - } - @GET @Path("descriptor") @Produces(MediaType.APPLICATION_XML) @@ -537,7 +526,7 @@ public class SamlService extends AuthorizationEndpointBase { } ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(getLoginProtocol()); + clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING); @@ -555,8 +544,14 @@ public class SamlService extends AuthorizationEndpointBase { } - protected String getLoginProtocol() { - return SamlProtocol.LOGIN_PROTOCOL; + @POST + @Consumes("application/soap+xml") + public Response soapBinding(InputStream inputStream) { + SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, authManager); + + ResteasyProviderFactory.getInstance().injectProperties(bindingService); + + return bindingService.authenticate(inputStream); } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java deleted file mode 100644 index dba1b295b9..0000000000 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.keycloak.protocol.saml.profile.ecp; - -import org.keycloak.Config; -import org.keycloak.events.EventBuilder; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.protocol.LoginProtocol; -import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; -import org.keycloak.protocol.saml.SamlProtocol; -import org.keycloak.protocol.saml.SamlProtocolFactory; -import org.keycloak.protocol.saml.profile.ecp.util.Soap; -import org.keycloak.protocol.saml.profile.ecp.util.Soap.SoapMessageBuilder; -import org.keycloak.saml.SAML2LogoutResponseBuilder; -import org.keycloak.saml.common.constants.JBossSAMLConstants; -import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.services.managers.AuthenticationManager; -import org.w3c.dom.Document; - -import javax.ws.rs.core.Response; -import javax.xml.soap.SOAPException; -import javax.xml.soap.SOAPHeaderElement; -import java.io.IOException; - -/** - * @author Pedro Igor - */ -public class SamlEcpProfileProtocolFactory extends SamlProtocolFactory { - - static final String ID = "saml-ecp-profile"; - - private static final String NS_PREFIX_PROFILE_ECP = "ecp"; - private static final String NS_PREFIX_SAML_PROTOCOL = "samlp"; - private static final String NS_PREFIX_SAML_ASSERTION = "saml"; - - @Override - public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { - return new SamlEcpProfileService(realm, event, authManager); - } - - @Override - public LoginProtocol create(KeycloakSession session) { - return new SamlProtocol() { - // method created to send a SOAP Binding response instead of a HTTP POST response - @Override - protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { - Document document = bindingBuilder.postBinding(samlDocument).getDocument(); - - try { - SoapMessageBuilder messageBuilder = Soap.createMessage() - .addNamespace(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get()) - .addNamespace(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get()) - .addNamespace(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get()); - - createEcpResponseHeader(redirectUri, messageBuilder); - createRequestAuthenticatedHeader(clientSession, messageBuilder); - - messageBuilder.addToBody(document); - - return messageBuilder.build(); - } catch (Exception e) { - throw new RuntimeException("Error while creating SAML response.", e); - } - } - - private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, SoapMessageBuilder messageBuilder) { - ClientModel client = clientSession.getClient(); - - if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { - SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP); - - ecpRequestAuthenticated.setMustUnderstand(true); - ecpRequestAuthenticated.setActor("http://schemas.xmlsoap.org/soap/actor/next"); - } - } - - private void createEcpResponseHeader(String redirectUri, SoapMessageBuilder messageBuilder) throws SOAPException { - SOAPHeaderElement ecpResponseHeader = messageBuilder.addHeader(JBossSAMLConstants.RESPONSE.get(), NS_PREFIX_PROFILE_ECP); - - ecpResponseHeader.setMustUnderstand(true); - ecpResponseHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next"); - ecpResponseHeader.addAttribute(messageBuilder.createName(JBossSAMLConstants.ASSERTION_CONSUMER_SERVICE_URL.get()), redirectUri); - } - - @Override - protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { - return Soap.createMessage().addToBody(document).build(); - } - - @Override - protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException { - return Soap.createFault().reason("Logout not supported.").build(); - } - }.setSession(session); - } - - @Override - public void init(Config.Scope config) { - } - - @Override - public String getId() { - return ID; - } -} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java index c16b997757..52b855702b 100644 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java @@ -6,13 +6,24 @@ import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlService; import org.keycloak.protocol.saml.profile.ecp.util.Soap; +import org.keycloak.saml.SAML2LogoutResponseBuilder; +import org.keycloak.saml.common.constants.JBossSAMLConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.services.managers.AuthenticationManager; +import org.w3c.dom.Document; import javax.ws.rs.core.Response; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPHeaderElement; +import java.io.IOException; import java.io.InputStream; /** @@ -20,6 +31,10 @@ import java.io.InputStream; */ public class SamlEcpProfileService extends SamlService { + private static final String NS_PREFIX_PROFILE_ECP = "ecp"; + private static final String NS_PREFIX_SAML_PROTOCOL = "samlp"; + private static final String NS_PREFIX_SAML_ASSERTION = "saml"; + public SamlEcpProfileService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { super(realm, event, authManager); } @@ -53,8 +68,63 @@ public class SamlEcpProfileService extends SamlService { } @Override - protected String getLoginProtocol() { - return SamlEcpProfileProtocolFactory.ID; + protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, SamlProtocol samlProtocol) { + return super.newBrowserAuthentication(clientSession, isPassive, createEcpSamlProtocol()); + } + + private SamlProtocol createEcpSamlProtocol() { + return new SamlProtocol() { + // method created to send a SOAP Binding response instead of a HTTP POST response + @Override + protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { + Document document = bindingBuilder.postBinding(samlDocument).getDocument(); + + try { + Soap.SoapMessageBuilder messageBuilder = Soap.createMessage() + .addNamespace(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get()) + .addNamespace(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get()) + .addNamespace(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get()); + + createEcpResponseHeader(redirectUri, messageBuilder); + createRequestAuthenticatedHeader(clientSession, messageBuilder); + + messageBuilder.addToBody(document); + + return messageBuilder.build(); + } catch (Exception e) { + throw new RuntimeException("Error while creating SAML response.", e); + } + } + + private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, Soap.SoapMessageBuilder messageBuilder) { + ClientModel client = clientSession.getClient(); + + if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { + SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP); + + ecpRequestAuthenticated.setMustUnderstand(true); + ecpRequestAuthenticated.setActor("http://schemas.xmlsoap.org/soap/actor/next"); + } + } + + private void createEcpResponseHeader(String redirectUri, Soap.SoapMessageBuilder messageBuilder) throws SOAPException { + SOAPHeaderElement ecpResponseHeader = messageBuilder.addHeader(JBossSAMLConstants.RESPONSE.get(), NS_PREFIX_PROFILE_ECP); + + ecpResponseHeader.setMustUnderstand(true); + ecpResponseHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next"); + ecpResponseHeader.addAttribute(messageBuilder.createName(JBossSAMLConstants.ASSERTION_CONSUMER_SERVICE_URL.get()), redirectUri); + } + + @Override + protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { + return Soap.createMessage().addToBody(document).build(); + } + + @Override + protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException { + return Soap.createFault().reason("Logout not supported.").build(); + } + }.setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo); } @Override diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory index ae434f63eb..d0a2dd046f 100755 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory @@ -1,2 +1 @@ -org.keycloak.protocol.saml.SamlProtocolFactory -org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileProtocolFactory \ No newline at end of file +org.keycloak.protocol.saml.SamlProtocolFactory \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 75803f1a6d..17c02afe33 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -810,10 +810,10 @@ public class AuthenticationProcessor { AuthenticationManager.evaluateRequiredActionTriggers(session, userSession, clientSession, connection, request, uriInfo, event, realm, clientSession.getAuthenticatedUser()); } - public Response finishAuthentication() { + public Response finishAuthentication(LoginProtocol protocol) { event.success(); RealmModel realm = clientSession.getRealm(); - return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection, event); + return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection, event, protocol); } diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index 9dc5548f57..befd364b78 100644 --- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -115,7 +115,7 @@ public abstract class AuthorizationEndpointBase { else return protocol.sendError(clientSession, Error.PASSIVE_INTERACTION_REQUIRED); } else { - return processor.finishAuthentication(); + return processor.finishAuthentication(protocol); } } else { try { 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 0131b6d206..d034d753e8 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -382,6 +382,19 @@ public class AuthenticationManager { ClientSessionModel clientSession, HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, EventBuilder event) { + LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); + protocol.setRealm(realm) + .setHttpHeaders(request.getHttpHeaders()) + .setUriInfo(uriInfo) + .setEventBuilder(event); + return redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event, protocol); + + } + + public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession, + ClientSessionModel clientSession, + HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, + EventBuilder event, LoginProtocol protocol) { Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE); if (sessionCookie != null) { @@ -405,12 +418,6 @@ public class AuthenticationManager { createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection); if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN); if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection); - LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); - protocol.setRealm(realm) - .setHttpHeaders(request.getHttpHeaders()) - .setUriInfo(uriInfo) - .setEventBuilder(event); - RestartLoginCookie.expireRestartCookie(realm, clientConnection, uriInfo); return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession)); } From 41d22986d50e518300391ffe8490bb8b32c5f1f8 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 22 Dec 2015 16:22:02 +0100 Subject: [PATCH 28/65] KEYCLOAK-1899 Added HardcodedLDAPRoleMapper --- .../broker/provider/HardcodedRoleMapper.java | 37 +----- .../oidc/mappers/ClaimToRoleMapper.java | 5 +- .../ExternalKeycloakRoleToRoleMapper.java | 5 +- .../saml/mappers/AttributeToRoleMapper.java | 5 +- .../FullNameLDAPFederationMapperFactory.java | 2 +- .../ldap/mappers/HardcodedLDAPRoleMapper.java | 109 ++++++++++++++++++ .../HardcodedLDAPRoleMapperFactory.java | 78 +++++++++++++ ...rAttributeLDAPFederationMapperFactory.java | 2 +- .../GroupLDAPFederationMapperFactory.java | 2 +- .../role/RoleLDAPFederationMapperFactory.java | 2 +- ...ycloak.mappers.UserFederationMapperFactory | 1 + .../mappers/UserFederationMapperFactory.java | 3 +- .../models/utils/KeycloakModelUtils.java | 30 +++++ .../protocol/ProtocolMapperUtils.java | 14 --- .../protocol/oidc/mappers/HardcodedRole.java | 3 +- .../protocol/oidc/mappers/RoleNameMapper.java | 5 +- .../admin/UserFederationProviderResource.java | 2 +- .../FederationProvidersIntegrationTest.java | 48 ++++++++ 18 files changed, 290 insertions(+), 63 deletions(-) create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapper.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java b/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java index 309e26a2bc..6f0b02a5e0 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java @@ -1,14 +1,11 @@ package org.keycloak.broker.provider; -import org.keycloak.broker.provider.AbstractIdentityProviderMapper; -import org.keycloak.broker.provider.BrokeredIdentityContext; -import org.keycloak.broker.provider.IdentityBrokerException; -import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; @@ -20,7 +17,7 @@ import java.util.List; */ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { public static final String ROLE = "role"; - protected static final List configProperties = new ArrayList(); + protected static final List configProperties = new ArrayList<>(); static { ProviderConfigProperty property; @@ -32,34 +29,6 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { configProperties.add(property); } - - - public static String[] parseRole(String role) { - int scopeIndex = role.lastIndexOf('.'); - if (scopeIndex > -1) { - String appName = role.substring(0, scopeIndex); - role = role.substring(scopeIndex + 1); - String[] rtn = {appName, role}; - return rtn; - } else { - String[] rtn = {null, role}; - return rtn; - - } - } - - public static RoleModel getRoleFromString(RealmModel realm, String roleName) { - String[] parsedRole = parseRole(roleName); - RoleModel role = null; - if (parsedRole[0] == null) { - role = realm.getRole(parsedRole[1]); - } else { - ClientModel client = realm.getClientByClientId(parsedRole[0]); - role = client.getRole(parsedRole[1]); - } - return role; - } - @Override public List getConfigProperties() { return configProperties; @@ -93,7 +62,7 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(ROLE); - RoleModel role = getRoleFromString(realm, roleName); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); user.grantRole(role); } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java index b3bd191d27..2d79a5b22b 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java @@ -10,6 +10,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; @@ -80,7 +81,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper { public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE); if (hasClaimValue(mapperModel, context)) { - RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); user.grantRole(role); } @@ -90,7 +91,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE); if (!hasClaimValue(mapperModel, context)) { - RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); user.deleteRoleMapping(role); } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java index e4e0de020b..f7ddb76f2c 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java @@ -11,6 +11,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.JsonWebToken; @@ -85,7 +86,7 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper { JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN); //if (token == null) return; String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE); - String[] parseRole = HardcodedRoleMapper.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE)); + String[] parseRole = KeycloakModelUtils.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE)); String externalRoleName = parseRole[1]; String claimName = null; if (parseRole[0] == null) { @@ -95,7 +96,7 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper { } Object claim = getClaimValue(token, claimName); if (valueEquals(externalRoleName, claim)) { - RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); return role; } diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java index 4014a9a844..48d2ac89d7 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java @@ -15,6 +15,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.JsonWebToken; @@ -95,7 +96,7 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper { public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE); if (isAttributePresent(mapperModel, context)) { - RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); user.grantRole(role); } @@ -125,7 +126,7 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE); if (!isAttributePresent(mapperModel, context)) { - RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); user.deleteRoleMapping(role); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java index e08429ef23..7fbcf06318 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java @@ -75,7 +75,7 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM } @Override - public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { checkMandatoryConfigAttribute(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", mapperModel); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapper.java new file mode 100644 index 0000000000..866d2b1d77 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapper.java @@ -0,0 +1,109 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.UserModelDelegate; + +/** + * @author Marek Posolda + */ +public class HardcodedLDAPRoleMapper extends AbstractLDAPFederationMapper { + + private static final Logger logger = Logger.getLogger(HardcodedLDAPRoleMapper.class); + + public static final String ROLE = "role"; + + public HardcodedLDAPRoleMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { + super(mapperModel, ldapProvider, realm); + } + + @Override + public void beforeLDAPQuery(LDAPQuery query) { + } + + @Override + public UserModel proxy(LDAPObject ldapUser, UserModel delegate) { + return new UserModelDelegate(delegate) { + + @Override + public Set getRealmRoleMappings() { + Set roles = super.getRealmRoleMappings(); + + RoleModel role = getRole(); + if (role != null && role.getContainer().equals(realm)) { + roles.add(role); + } + + return roles; + } + + @Override + public Set getClientRoleMappings(ClientModel app) { + Set roles = super.getClientRoleMappings(app); + + RoleModel role = getRole(); + if (role != null && role.getContainer().equals(app)) { + roles.add(role); + } + + return roles; + } + + @Override + public boolean hasRole(RoleModel role) { + return super.hasRole(role) || role.equals(getRole()); + } + + @Override + public Set getRoleMappings() { + Set roles = super.getRoleMappings(); + + RoleModel role = getRole(); + if (role != null) { + roles.add(role); + } + + return roles; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + if (role.equals(getRole())) { + throw new ModelException("Not possible to delete role. It's hardcoded by LDAP mapper"); + } else { + super.deleteRoleMapping(role); + } + } + }; + } + + @Override + public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) { + + } + + @Override + public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) { + + } + + private RoleModel getRole() { + String roleName = mapperModel.getConfig().get(HardcodedLDAPRoleMapper.ROLE); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) { + logger.warnf("Hardcoded role '%s' configured in mapper '%s' is not available anymore"); + } + return role; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java new file mode 100644 index 0000000000..508a333417 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java @@ -0,0 +1,78 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.mappers.MapperConfigValidationException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Marek Posolda + */ +public class HardcodedLDAPRoleMapperFactory extends AbstractLDAPFederationMapperFactory { + + public static final String PROVIDER_ID = "hardcoded-ldap-role-mapper"; + protected static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty roleAttr = createConfigProperty(HardcodedLDAPRoleMapper.ROLE, "Role", + "Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole", + ProviderConfigProperty.ROLE_TYPE, null); + configProperties.add(roleAttr); + } + + @Override + public String getHelpText() { + return "When user is imported from LDAP, he will be automatically added into this configured role."; + } + + @Override + public String getDisplayCategory() { + return ROLE_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Hardcoded Role"; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public Map getDefaultConfig(UserFederationProviderModel providerModel) { + return new HashMap<>(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + String roleName = mapperModel.getConfig().get(HardcodedLDAPRoleMapper.ROLE); + if (roleName == null) { + throw new MapperConfigValidationException("Role can't be null"); + } + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) { + throw new MapperConfigValidationException("There is no role corresponding to configured value"); + } + } + + @Override + protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) { + return new HardcodedLDAPRoleMapper(mapperModel, federationProvider, realm); + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java index 9b061a6a44..5d929007d7 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java @@ -87,7 +87,7 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera } @Override - public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", mapperModel); checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute", mapperModel); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java index 464cc0d92e..fa43ac9721 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java @@ -168,7 +168,7 @@ public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapp } @Override - public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", mapperModel); checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", mapperModel); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java index a4ba60f3a8..fe0b1e194c 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java @@ -161,7 +161,7 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe } @Override - public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", mapperModel); checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", mapperModel); diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory index 9e4a658dd7..ac130a551e 100644 --- a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory +++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory @@ -1,4 +1,5 @@ org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory +org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java index f009c3e2d6..eebc3d7d3e 100644 --- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java @@ -2,6 +2,7 @@ package org.keycloak.mappers; import java.util.Map; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.provider.ConfiguredProvider; @@ -37,7 +38,7 @@ public interface UserFederationMapperFactory extends ProviderFactory -1) { + String appName = role.substring(0, scopeIndex); + role = role.substring(scopeIndex + 1); + String[] rtn = {appName, role}; + return rtn; + } else { + String[] rtn = {null, role}; + return rtn; + + } + } + } diff --git a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java index 2b8677363c..9022c269a6 100755 --- a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java +++ b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java @@ -47,20 +47,6 @@ public class ProtocolMapperUtils { return null; } - public static String[] parseRole(String role) { - int scopeIndex = role.lastIndexOf('.'); - if (scopeIndex > -1) { - String appName = role.substring(0, scopeIndex); - role = role.substring(scopeIndex + 1); - String[] rtn = {appName, role}; - return rtn; - } else { - String[] rtn = {null, role}; - return rtn; - - } - } - /** * Find the builtin locale mapper. * diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java index c49eebbf21..a839e31393 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java @@ -4,6 +4,7 @@ import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; @@ -67,7 +68,7 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { String role = mappingModel.getConfig().get(ROLE_CONFIG); - String[] scopedRole = ProtocolMapperUtils.parseRole(role); + String[] scopedRole = KeycloakModelUtils.parseRole(role); String appName = scopedRole[0]; String roleName = scopedRole[1]; if (appName != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java index 3ce1e19d5f..5f0178cc6f 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java @@ -5,6 +5,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; @@ -78,8 +79,8 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc String role = mappingModel.getConfig().get(ROLE_CONFIG); String newName = mappingModel.getConfig().get(NEW_ROLE_NAME); - String[] scopedRole = ProtocolMapperUtils.parseRole(role); - String[] newScopedRole = ProtocolMapperUtils.parseRole(newName); + String[] scopedRole = KeycloakModelUtils.parseRole(role); + String[] newScopedRole = KeycloakModelUtils.parseRole(newName); String appName = scopedRole[0]; String roleName = scopedRole[1]; if (appName != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java index 104627685a..29402474df 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java @@ -353,7 +353,7 @@ public class UserFederationProviderResource { private void validateModel(UserFederationMapperModel model) { try { UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType()); - mapperFactory.validateConfig(model); + mapperFactory.validateConfig(realm, model); } catch (MapperConfigValidationException ex) { throw new ErrorResponseException("Validation error", ex.getMessage(), Response.Status.BAD_REQUEST); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java index 5702a451a5..1d41595f6f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java @@ -15,11 +15,15 @@ import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapper; +import org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; import org.keycloak.models.ModelReadOnlyException; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationMapperModel; @@ -524,6 +528,50 @@ public class FederationProvidersIntegrationTest { } } + @Test + public void testHardcodedRoleMapper() { + KeycloakSession session = keycloakRule.startSession(); + UserFederationMapperModel firstNameMapper = null; + + try { + RealmModel appRealm = new RealmManager(session).getRealmByName("test"); + RoleModel hardcodedRole = appRealm.addRole("hardcoded-role"); + + // assert that user "johnkeycloak" doesn't have hardcoded role + UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); + Assert.assertFalse(john.hasRole(hardcodedRole)); + + UserFederationMapperModel hardcodedMapperModel = KeycloakModelUtils.createUserFederationMapperModel("hardcoded role", ldapModel.getId(), HardcodedLDAPRoleMapperFactory.PROVIDER_ID, + HardcodedLDAPRoleMapper.ROLE, "hardcoded-role"); + appRealm.addUserFederationMapper(hardcodedMapperModel); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel appRealm = new RealmManager(session).getRealmByName("test"); + RoleModel hardcodedRole = appRealm.getRole("hardcoded-role"); + + // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName + UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); + Assert.assertTrue(john.hasRole(hardcodedRole)); + + // Can't remove user from hardcoded role + try { + john.deleteRoleMapping(hardcodedRole); + Assert.fail("Didn't expected to remove role mapping"); + } catch (ModelException expected) { + } + + // Revert mappers + UserFederationMapperModel hardcodedMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "hardcoded role"); + appRealm.removeUserFederationMapper(hardcodedMapperModel); + } finally { + keycloakRule.stopSession(session, true); + } + } + @Test public void testImportExistingUserFromLDAP() throws Exception { // Add LDAP user with same email like existing model user From dbac1474197102e235b567e3878002cb6b7d33fb Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 22 Dec 2015 17:50:03 -0500 Subject: [PATCH 29/65] client config refactor --- .../META-INF/jpa-changelog-1.8.0.xml | 35 +++++ .../keycloak/models/ClientTemplateModel.java | 30 +++++ .../models/entities/ClientTemplateEntity.java | 85 +++++++++++- .../infinispan/ClientTemplateAdapter.java | 121 ++++++++++++++++++ .../cache/entities/CachedClientTemplate.java | 54 ++++++++ .../models/jpa/ClientTemplateAdapter.java | 105 +++++++++++++++ .../jpa/entities/ClientTemplateEntity.java | 101 +++++++++++++++ .../adapters/ClientTemplateAdapter.java | 115 +++++++++++++++++ .../EntityDescriptorDescriptionConverter.java | 16 +-- .../keycloak/protocol/saml/SamlClient.java | 73 ++++++----- .../saml/SamlClientRepresentation.java | 17 ++- .../protocol/saml/SamlConfigAttributes.java | 22 ++++ .../keycloak/protocol/saml/SamlProtocol.java | 94 ++++---------- .../protocol/saml/SamlProtocolFactory.java | 2 + .../protocol/saml/SamlProtocolUtils.java | 9 +- .../keycloak/protocol/saml/SamlService.java | 18 +-- .../ecp/SamlEcpProfileProtocolFactory.java | 4 +- 17 files changed, 769 insertions(+), 132 deletions(-) create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java mode change 100644 => 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml index 068d4e7187..d0893a3830 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml @@ -18,6 +18,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -69,6 +102,8 @@ + + diff --git a/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java index f3c0f59954..eb3fbc0342 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java @@ -23,5 +23,35 @@ public interface ClientTemplateModel extends ProtocolMapperContainerModel, Scope String getProtocol(); void setProtocol(String protocol); + void setAttribute(String name, String value); + void removeAttribute(String name); + String getAttribute(String name); + Map getAttributes(); + + boolean isFrontchannelLogout(); + void setFrontchannelLogout(boolean flag); + + boolean isBearerOnly(); + void setBearerOnly(boolean only); + + boolean isPublicClient(); + void setPublicClient(boolean flag); + + boolean isConsentRequired(); + void setConsentRequired(boolean consentRequired); + + boolean isStandardFlowEnabled(); + void setStandardFlowEnabled(boolean standardFlowEnabled); + + boolean isImplicitFlowEnabled(); + void setImplicitFlowEnabled(boolean implicitFlowEnabled); + + boolean isDirectAccessGrantsEnabled(); + void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled); + + boolean isServiceAccountsEnabled(); + void setServiceAccountsEnabled(boolean serviceAccountsEnabled); + + } diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java index 8ca932f0f1..a9879f7657 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java @@ -15,8 +15,17 @@ public class ClientTemplateEntity extends AbstractIdentifiableEntity { private String realmId; private String protocol; private boolean fullScopeAllowed; - private List scopeIds = new ArrayList(); - private List protocolMappers = new ArrayList(); + private boolean bearerOnly; + private boolean consentRequired; + private boolean standardFlowEnabled; + private boolean implicitFlowEnabled; + private boolean directAccessGrantsEnabled; + private boolean serviceAccountsEnabled; + private boolean publicClient; + private boolean frontchannelLogout; + private List scopeIds = new ArrayList<>(); + private List protocolMappers = new ArrayList<>(); + private Map attributes = new HashMap<>(); public String getName() { return name; @@ -73,5 +82,77 @@ public class ClientTemplateEntity extends AbstractIdentifiableEntity { public void setScopeIds(List scopeIds) { this.scopeIds = scopeIds; } + + public boolean isBearerOnly() { + return bearerOnly; + } + + public void setBearerOnly(boolean bearerOnly) { + this.bearerOnly = bearerOnly; + } + + public boolean isConsentRequired() { + return consentRequired; + } + + public void setConsentRequired(boolean consentRequired) { + this.consentRequired = consentRequired; + } + + public boolean isStandardFlowEnabled() { + return standardFlowEnabled; + } + + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + this.standardFlowEnabled = standardFlowEnabled; + } + + public boolean isImplicitFlowEnabled() { + return implicitFlowEnabled; + } + + public void setImplicitFlowEnabled(boolean implicitFlowEnabled) { + this.implicitFlowEnabled = implicitFlowEnabled; + } + + public boolean isDirectAccessGrantsEnabled() { + return directAccessGrantsEnabled; + } + + public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) { + this.directAccessGrantsEnabled = directAccessGrantsEnabled; + } + + public boolean isServiceAccountsEnabled() { + return serviceAccountsEnabled; + } + + public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { + this.serviceAccountsEnabled = serviceAccountsEnabled; + } + + public boolean isPublicClient() { + return publicClient; + } + + public void setPublicClient(boolean publicClient) { + this.publicClient = publicClient; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public boolean isFrontchannelLogout() { + return frontchannelLogout; + } + + public void setFrontchannelLogout(boolean frontchannelLogout) { + this.frontchannelLogout = frontchannelLogout; + } } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java index 13b68aa881..d403adc170 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java @@ -194,6 +194,127 @@ public class ClientTemplateAdapter implements ClientTemplateModel { return false; } + public boolean isPublicClient() { + if (updated != null) return updated.isPublicClient(); + return cached.isPublicClient(); + } + + public void setPublicClient(boolean flag) { + getDelegateForUpdate(); + updated.setPublicClient(flag); + } + + public boolean isFrontchannelLogout() { + if (updated != null) return updated.isPublicClient(); + return cached.isFrontchannelLogout(); + } + + public void setFrontchannelLogout(boolean flag) { + getDelegateForUpdate(); + updated.setFrontchannelLogout(flag); + } + + @Override + public void setAttribute(String name, String value) { + getDelegateForUpdate(); + updated.setAttribute(name, value); + + } + + @Override + public void removeAttribute(String name) { + getDelegateForUpdate(); + updated.removeAttribute(name); + + } + + @Override + public String getAttribute(String name) { + if (updated != null) return updated.getAttribute(name); + return cached.getAttributes().get(name); + } + + @Override + public Map getAttributes() { + if (updated != null) return updated.getAttributes(); + Map copy = new HashMap(); + copy.putAll(cached.getAttributes()); + return copy; + } + + @Override + public boolean isBearerOnly() { + if (updated != null) return updated.isBearerOnly(); + return cached.isBearerOnly(); + } + + @Override + public void setBearerOnly(boolean only) { + getDelegateForUpdate(); + updated.setBearerOnly(only); + } + + @Override + public boolean isConsentRequired() { + if (updated != null) return updated.isConsentRequired(); + return cached.isConsentRequired(); + } + + @Override + public void setConsentRequired(boolean consentRequired) { + getDelegateForUpdate(); + updated.setConsentRequired(consentRequired); + } + + @Override + public boolean isStandardFlowEnabled() { + if (updated != null) return updated.isStandardFlowEnabled(); + return cached.isStandardFlowEnabled(); + } + + @Override + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + getDelegateForUpdate(); + updated.setStandardFlowEnabled(standardFlowEnabled); + } + + @Override + public boolean isImplicitFlowEnabled() { + if (updated != null) return updated.isImplicitFlowEnabled(); + return cached.isImplicitFlowEnabled(); + } + + @Override + public void setImplicitFlowEnabled(boolean implicitFlowEnabled) { + getDelegateForUpdate(); + updated.setImplicitFlowEnabled(implicitFlowEnabled); + } + + @Override + public boolean isDirectAccessGrantsEnabled() { + if (updated != null) return updated.isDirectAccessGrantsEnabled(); + return cached.isDirectAccessGrantsEnabled(); + } + + @Override + public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) { + getDelegateForUpdate(); + updated.setDirectAccessGrantsEnabled(directAccessGrantsEnabled); + } + + @Override + public boolean isServiceAccountsEnabled() { + if (updated != null) return updated.isServiceAccountsEnabled(); + return cached.isServiceAccountsEnabled(); + } + + @Override + public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { + getDelegateForUpdate(); + updated.setServiceAccountsEnabled(serviceAccountsEnabled); + } + + @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java index 58bcc126cd..b03430deee 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java @@ -29,8 +29,17 @@ public class CachedClientTemplate implements Serializable { private String realm; private String protocol; private boolean fullScopeAllowed; + private boolean publicClient; + private boolean frontchannelLogout; + private boolean bearerOnly; + private boolean consentRequired; + private boolean standardFlowEnabled; + private boolean implicitFlowEnabled; + private boolean directAccessGrantsEnabled; + private boolean serviceAccountsEnabled; private Set scope = new HashSet(); private Set protocolMappers = new HashSet(); + private Map attributes = new HashMap(); public CachedClientTemplate(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientTemplateModel model) { id = model.getId(); @@ -45,6 +54,15 @@ public class CachedClientTemplate implements Serializable { for (RoleModel role : model.getScopeMappings()) { scope.add(role.getId()); } + attributes.putAll(model.getAttributes()); + frontchannelLogout = model.isFrontchannelLogout(); + publicClient = model.isPublicClient(); + bearerOnly = model.isBearerOnly(); + consentRequired = model.isConsentRequired(); + standardFlowEnabled = model.isStandardFlowEnabled(); + implicitFlowEnabled = model.isImplicitFlowEnabled(); + directAccessGrantsEnabled = model.isDirectAccessGrantsEnabled(); + serviceAccountsEnabled = model.isServiceAccountsEnabled(); } public String getId() { return id; @@ -77,4 +95,40 @@ public class CachedClientTemplate implements Serializable { public Set getScope() { return scope; } + + public boolean isPublicClient() { + return publicClient; + } + + public boolean isFrontchannelLogout() { + return frontchannelLogout; + } + + public boolean isBearerOnly() { + return bearerOnly; + } + + public boolean isConsentRequired() { + return consentRequired; + } + + public boolean isStandardFlowEnabled() { + return standardFlowEnabled; + } + + public boolean isImplicitFlowEnabled() { + return implicitFlowEnabled; + } + + public boolean isDirectAccessGrantsEnabled() { + return directAccessGrantsEnabled; + } + + public boolean isServiceAccountsEnabled() { + return serviceAccountsEnabled; + } + + public Map getAttributes() { + return attributes; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java index 2d5c78b7c2..4e70debd06 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java @@ -287,6 +287,111 @@ public class ClientTemplateAdapter implements ClientTemplateModel { return false; } + @Override + public boolean isPublicClient() { + return entity.isPublicClient(); + } + + @Override + public void setPublicClient(boolean flag) { + entity.setPublicClient(flag); + } + + @Override + public boolean isFrontchannelLogout() { + return entity.isFrontchannelLogout(); + } + + @Override + public void setFrontchannelLogout(boolean flag) { + entity.setFrontchannelLogout(flag); + } + + @Override + public void setAttribute(String name, String value) { + entity.getAttributes().put(name, value); + + } + + @Override + public void removeAttribute(String name) { + entity.getAttributes().remove(name); + } + + @Override + public String getAttribute(String name) { + return entity.getAttributes().get(name); + } + + @Override + public Map getAttributes() { + Map copy = new HashMap<>(); + copy.putAll(entity.getAttributes()); + return copy; + } + + @Override + public boolean isBearerOnly() { + return entity.isBearerOnly(); + } + + @Override + public void setBearerOnly(boolean only) { + entity.setBearerOnly(only); + } + + @Override + public boolean isConsentRequired() { + return entity.isConsentRequired(); + } + + @Override + public void setConsentRequired(boolean consentRequired) { + entity.setConsentRequired(consentRequired); + } + + @Override + public boolean isStandardFlowEnabled() { + return entity.isStandardFlowEnabled(); + } + + @Override + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + entity.setStandardFlowEnabled(standardFlowEnabled); + } + + @Override + public boolean isImplicitFlowEnabled() { + return entity.isImplicitFlowEnabled(); + } + + @Override + public void setImplicitFlowEnabled(boolean implicitFlowEnabled) { + entity.setImplicitFlowEnabled(implicitFlowEnabled); + } + + @Override + public boolean isDirectAccessGrantsEnabled() { + return entity.isDirectAccessGrantsEnabled(); + } + + @Override + public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) { + entity.setDirectAccessGrantsEnabled(directAccessGrantsEnabled); + } + + @Override + public boolean isServiceAccountsEnabled() { + return entity.isServiceAccountsEnabled(); + } + + @Override + public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { + entity.setServiceAccountsEnabled(serviceAccountsEnabled); + } + + + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java index 5da01c4c57..ba3fc623be 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java @@ -48,6 +48,35 @@ public class ClientTemplateEntity { @Column(name="FULL_SCOPE_ALLOWED") private boolean fullScopeAllowed; + @Column(name="CONSENT_REQUIRED") + private boolean consentRequired; + + @Column(name="STANDARD_FLOW_ENABLED") + private boolean standardFlowEnabled; + + @Column(name="IMPLICIT_FLOW_ENABLED") + private boolean implicitFlowEnabled; + + @Column(name="DIRECT_ACCESS_GRANTS_ENABLED") + private boolean directAccessGrantsEnabled; + + @Column(name="SERVICE_ACCOUNTS_ENABLED") + private boolean serviceAccountsEnabled; + + @Column(name="FRONTCHANNEL_LOGOUT") + private boolean frontchannelLogout; + @Column(name="PUBLIC_CLIENT") + private boolean publicClient; + @Column(name="BEARER_ONLY") + private boolean bearerOnly; + + + @ElementCollection + @MapKeyColumn(name="NAME") + @Column(name="VALUE", length = 2048) + @CollectionTable(name="CLIENT_TEMPLATE_ATTRIBUTES", joinColumns={ @JoinColumn(name="TEMPLATE_ID") }) + protected Map attributes = new HashMap(); + public RealmEntity getRealm() { return realm; } @@ -103,4 +132,76 @@ public class ClientTemplateEntity { public void setFullScopeAllowed(boolean fullScopeAllowed) { this.fullScopeAllowed = fullScopeAllowed; } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public boolean isConsentRequired() { + return consentRequired; + } + + public void setConsentRequired(boolean consentRequired) { + this.consentRequired = consentRequired; + } + + public boolean isStandardFlowEnabled() { + return standardFlowEnabled; + } + + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + this.standardFlowEnabled = standardFlowEnabled; + } + + public boolean isImplicitFlowEnabled() { + return implicitFlowEnabled; + } + + public void setImplicitFlowEnabled(boolean implicitFlowEnabled) { + this.implicitFlowEnabled = implicitFlowEnabled; + } + + public boolean isDirectAccessGrantsEnabled() { + return directAccessGrantsEnabled; + } + + public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) { + this.directAccessGrantsEnabled = directAccessGrantsEnabled; + } + + public boolean isServiceAccountsEnabled() { + return serviceAccountsEnabled; + } + + public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { + this.serviceAccountsEnabled = serviceAccountsEnabled; + } + + public boolean isFrontchannelLogout() { + return frontchannelLogout; + } + + public void setFrontchannelLogout(boolean frontchannelLogout) { + this.frontchannelLogout = frontchannelLogout; + } + + public boolean isPublicClient() { + return publicClient; + } + + public void setPublicClient(boolean publicClient) { + this.publicClient = publicClient; + } + + public boolean isBearerOnly() { + return bearerOnly; + } + + public void setBearerOnly(boolean bearerOnly) { + this.bearerOnly = bearerOnly; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java index 91725bffd0..9b2d9215ac 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java @@ -274,6 +274,121 @@ public class ClientTemplateAdapter extends AbstractMongoAdapter getAttributes() { + Map copy = new HashMap(); + copy.putAll(getMongoEntity().getAttributes()); + return copy; + } + + @Override + public boolean isBearerOnly() { + return getMongoEntity().isBearerOnly(); + } + + @Override + public void setBearerOnly(boolean only) { + getMongoEntity().setBearerOnly(only); + updateMongoEntity(); + } + + @Override + public boolean isConsentRequired() { + return getMongoEntity().isConsentRequired(); + } + + @Override + public void setConsentRequired(boolean consentRequired) { + getMongoEntity().setConsentRequired(consentRequired); + updateMongoEntity(); + } + + @Override + public boolean isStandardFlowEnabled() { + return getMongoEntity().isStandardFlowEnabled(); + } + + @Override + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + getMongoEntity().setStandardFlowEnabled(standardFlowEnabled); + updateMongoEntity(); + } + + @Override + public boolean isImplicitFlowEnabled() { + return getMongoEntity().isImplicitFlowEnabled(); + } + + @Override + public void setImplicitFlowEnabled(boolean implicitFlowEnabled) { + getMongoEntity().setImplicitFlowEnabled(implicitFlowEnabled); + updateMongoEntity(); + } + + @Override + public boolean isDirectAccessGrantsEnabled() { + return getMongoEntity().isDirectAccessGrantsEnabled(); + } + + @Override + public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) { + getMongoEntity().setDirectAccessGrantsEnabled(directAccessGrantsEnabled); + updateMongoEntity(); + } + + @Override + public boolean isServiceAccountsEnabled() { + return getMongoEntity().isServiceAccountsEnabled(); + } + + @Override + public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { + getMongoEntity().setServiceAccountsEnabled(serviceAccountsEnabled); + updateMongoEntity(); + } + + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java index 42ff3ee0c0..20f78109f4 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java @@ -4,10 +4,8 @@ import org.keycloak.Config; import org.keycloak.dom.saml.v2.metadata.*; import org.keycloak.exportimport.ClientDescriptionConverter; import org.keycloak.exportimport.ClientDescriptionConverterFactory; -import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.saml.SignatureAlgorithm; @@ -80,12 +78,12 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo app.setFullScopeAllowed(true); app.setProtocol(SamlProtocol.LOGIN_PROTOCOL); - attributes.put(SamlProtocol.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true - attributes.put(SamlProtocol.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString()); - attributes.put(SamlProtocol.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true + attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString()); + attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity); if (spDescriptorType.isWantAssertionsSigned()) { - attributes.put(SamlProtocol.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); } String logoutPost = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost); @@ -114,10 +112,10 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo } String certPem = KeycloakModelUtils.getPemFromCertificate(cert); if (keyDescriptor.getUse() == KeyTypes.SIGNING) { - attributes.put(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); - attributes.put(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem); + attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + attributes.put(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem); } else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) { - attributes.put(SamlProtocol.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + attributes.put(SamlConfigAttributes.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); attributes.put(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem); } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java index 241cc268f8..df27de2fbc 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java @@ -8,23 +8,31 @@ import org.keycloak.saml.SignatureAlgorithm; * @version $Revision: 1 $ */ public class SamlClient { - public static final String SAML_SIGNING_PRIVATE_KEY = "saml.signing.private.key"; protected ClientModel client; public SamlClient(ClientModel client) { this.client = client; } + public String getId() { + return client.getId(); + } + + public String getClientId() { + return client.getClientId(); + } +// + public String getCanonicalizationMethod() { - return client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + return client.getAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); } public void setCanonicalizationMethod(String value) { - client.setAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE, value); + client.setAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE, value); } public SignatureAlgorithm getSignatureAlgorithm() { - String alg = client.getAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM); + String alg = client.getAttribute(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM); if (alg != null) { SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); if (algorithm != null) @@ -34,94 +42,91 @@ public class SamlClient { } public void setSignatureAlgorithm(SignatureAlgorithm algorithm) { - client.setAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM, algorithm.name()); + client.setAttribute(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, algorithm.name()); } public String getNameIDFormat() { - return client.getAttributes().get(SamlProtocol.SAML_NAME_ID_FORMAT_ATTRIBUTE); + return client.getAttributes().get(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); } public void setNameIDFormat(String format) { - client.setAttribute(SamlProtocol.SAML_NAME_ID_FORMAT_ATTRIBUTE, format); + client.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, format); } public boolean includeAuthnStatement() { - return "true".equals(client.getAttribute(SamlProtocol.SAML_AUTHNSTATEMENT)); + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT)); } public void setIncludeAuthnStatement(boolean val) { - client.setAttribute(SamlProtocol.SAML_AUTHNSTATEMENT, Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, Boolean.toString(val)); } public boolean forceNameIDFormat() { - return "true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); } public void setForceNameIDFormat(boolean val) { - client.setAttribute(SamlProtocol.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val)); } - public boolean requiresRealmSignature(ClientModel client) { - return "true".equals(client.getAttribute(SamlProtocol.SAML_SERVER_SIGNATURE)); + public boolean requiresRealmSignature() { + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE)); } public void setRequiresRealmSignature(boolean val) { - client.setAttribute(SamlProtocol.SAML_SERVER_SIGNATURE, Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val)); } - public boolean forcePostBinding(ClientModel client) { - return "true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING)); + public boolean forcePostBinding() { + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING)); } public void setForcePostBinding(boolean val) { - client.setAttribute(SamlProtocol.SAML_FORCE_POST_BINDING, Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING, Boolean.toString(val)); } - public boolean samlAssertionSignature(ClientModel client) { - return "true".equals(client.getAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE)); + public boolean requiresAssertionSignature() { + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); } - public void setAssertionSignature(boolean val) { - client.setAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE , Boolean.toString(val)); + public void setRequiresAssertionSignature(boolean val) { + client.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE , Boolean.toString(val)); } - public boolean requiresEncryption(ClientModel client) { - return "true".equals(client.getAttribute(SamlProtocol.SAML_ENCRYPT)); + public boolean requiresEncryption() { + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_ENCRYPT)); } public void setRequiresEncryption(boolean val) { - client.setAttribute(SamlProtocol.SAML_ENCRYPT, Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, Boolean.toString(val)); } - public boolean requiresClientSignature(ClientModel client) { - return "true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE)); + public boolean requiresClientSignature() { + return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE)); } public void setRequiresClientSignature(boolean val) { - client.setAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE , Boolean.toString(val)); + client.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE , Boolean.toString(val)); } public String getClientSigningCertificate() { - return client.getAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE); + return client.getAttribute(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE); } public void setClientSigningCertificate(String val) { - client.setAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, val); + client.setAttribute(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, val); } public String getClientSigningPrivateKey() { - return client.getAttribute(SAML_SIGNING_PRIVATE_KEY); + return client.getAttribute(SamlConfigAttributes.SAML_SIGNING_PRIVATE_KEY); } public void setClientSigningPrivateKey(String val) { - client.setAttribute(SAML_SIGNING_PRIVATE_KEY, val); + client.setAttribute(SamlConfigAttributes.SAML_SIGNING_PRIVATE_KEY, val); } - - - } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java index 4151d625a7..0ce4beba43 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java @@ -1,6 +1,5 @@ package org.keycloak.protocol.saml; -import org.keycloak.models.ClientModel; import org.keycloak.representations.idm.ClientRepresentation; /** @@ -16,45 +15,45 @@ public class SamlClientRepresentation { public String getCanonicalizationMethod() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + return rep.getAttributes().get(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); } public String getSignatureAlgorithm() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_SIGNATURE_ALGORITHM); + return rep.getAttributes().get(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM); } public String getNameIDFormat() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_NAME_ID_FORMAT_ATTRIBUTE); + return rep.getAttributes().get(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); } public String getIncludeAuthnStatement() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_AUTHNSTATEMENT); + return rep.getAttributes().get(SamlConfigAttributes.SAML_AUTHNSTATEMENT); } public String getForceNameIDFormat() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE); + return rep.getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE); } public String getSamlServerSignature() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_SERVER_SIGNATURE); + return rep.getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE); } public String getForcePostBinding() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_FORCE_POST_BINDING); + return rep.getAttributes().get(SamlConfigAttributes.SAML_FORCE_POST_BINDING); } public String getClientSignature() { if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE); + return rep.getAttributes().get(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE); } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java new file mode 100755 index 0000000000..eea258ac0b --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java @@ -0,0 +1,22 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.services.resources.admin.ClientAttributeCertificateResource; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface SamlConfigAttributes { + String SAML_SIGNING_PRIVATE_KEY = "saml.signing.private.key"; + String SAML_CANONICALIZATION_METHOD_ATTRIBUTE = "saml_signature_canonicalization_method"; + String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; + String SAML_NAME_ID_FORMAT_ATTRIBUTE = "saml_name_id_format"; + String SAML_AUTHNSTATEMENT = "saml.authnstatement"; + String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format"; + String SAML_SERVER_SIGNATURE = "saml.server.signature"; + String SAML_FORCE_POST_BINDING = "saml.force.post.binding"; + String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; + String SAML_ENCRYPT = "saml.encrypt"; + String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature"; + String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE; +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 07c528ddd9..e7bd3ebd4d 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -70,28 +70,17 @@ public class SamlProtocol implements LoginProtocol { public static final String ATTRIBUTE_TRUE_VALUE = "true"; public static final String ATTRIBUTE_FALSE_VALUE = "false"; - public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE; public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE; - public static final String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature"; public static final String SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE = "saml_assertion_consumer_url_post"; public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE = "saml_assertion_consumer_url_redirect"; public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post"; public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect"; - public static final String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format"; - public static final String SAML_NAME_ID_FORMAT_ATTRIBUTE = "saml_name_id_format"; - public static final String SAML_CANONICALIZATION_METHOD_ATTRIBUTE = "saml_signature_canonicalization_method"; public static final String LOGIN_PROTOCOL = "saml"; public static final String SAML_BINDING = "saml_binding"; public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login"; public static final String SAML_POST_BINDING = "post"; public static final String SAML_SOAP_BINDING = "soap"; public static final String SAML_REDIRECT_BINDING = "get"; - public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; - public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; - public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement"; - public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm"; - public static final String SAML_ENCRYPT = "saml.encrypt"; - public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding"; public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID"; public static final String SAML_LOGOUT_BINDING = "saml.logout.binding"; public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID"; @@ -218,7 +207,8 @@ public class SamlProtocol implements LoginProtocol { protected boolean isPostBinding(ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); - return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client); + SamlClient samlClient = new SamlClient(client); + return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding(); } public static boolean isLogoutPostBindingForInitiator(UserSessionModel session) { @@ -228,6 +218,7 @@ public class SamlProtocol implements LoginProtocol { protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); + SamlClient samlClient = new SamlClient(client); String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE); String logoutRedirectUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE); @@ -238,7 +229,7 @@ public class SamlProtocol implements LoginProtocol { return false; } - if (forcePostBinding(client)) { + if (samlClient.forcePostBinding()) { return true; // configured to force a post binding and post binding logout url is not null } @@ -255,15 +246,11 @@ public class SamlProtocol implements LoginProtocol { } - public static boolean forcePostBinding(ClientModel client) { - return "true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING)); - } - - protected String getNameIdFormat(ClientSessionModel clientSession) { + protected String getNameIdFormat(SamlClient samlClient, ClientSessionModel clientSession) { String nameIdFormat = clientSession.getNote(GeneralConstants.NAMEID_FORMAT); - ClientModel client = clientSession.getClient(); - boolean forceFormat = forceNameIdFormat(client); - String configuredNameIdFormat = client.getAttribute(SAML_NAME_ID_FORMAT_ATTRIBUTE); + + boolean forceFormat = samlClient.forceNameIDFormat(); + String configuredNameIdFormat = samlClient.getNameIDFormat(); if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) { if (configuredNameIdFormat.equals("email")) { nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get(); @@ -282,11 +269,7 @@ public class SamlProtocol implements LoginProtocol { return nameIdFormat; } - public static boolean forceNameIdFormat(ClientModel client) { - return "true".equals(client.getAttribute(SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); - } - - protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) { + protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) { if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) { return userSession.getUser().getEmail(); } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) { @@ -315,11 +298,12 @@ public class SamlProtocol implements LoginProtocol { public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { ClientSessionModel clientSession = accessCode.getClientSession(); ClientModel client = clientSession.getClient(); + SamlClient samlClient = new SamlClient(client); String requestID = clientSession.getNote(SAML_REQUEST_ID); String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE); String redirectUri = clientSession.getRedirectUri(); String responseIssuer = getResponseIssuer(realm); - String nameIdFormat = getNameIdFormat(clientSession); + String nameIdFormat = getNameIdFormat(samlClient, clientSession); String nameId = getNameId(nameIdFormat, clientSession, userSession); // save NAME_ID and format in clientSession as they may be persistent or transient or email and not username @@ -330,7 +314,7 @@ public class SamlProtocol implements LoginProtocol { SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder(); builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()).sessionIndex(clientSession.getId()) .requestIssuer(clientSession.getClient().getClientId()).nameIdentifier(nameIdFormat, nameId).authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get()); - if (!includeAuthnStatement(client)) { + if (!samlClient.includeAuthnStatement()) { builder.disableAuthnStatement(true); } @@ -370,21 +354,21 @@ public class SamlProtocol implements LoginProtocol { JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(); bindingBuilder.relayState(relayState); - if (requiresRealmSignature(client)) { - String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + if (samlClient.requiresRealmSignature()) { + String canonicalization = samlClient.getCanonicalizationMethod(); if (canonicalization != null) { bindingBuilder.canonicalizationMethod(canonicalization); } - bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); + bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } - if (requiresAssertionSignature(client)) { - String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + if (samlClient.requiresAssertionSignature()) { + String canonicalization = samlClient.getCanonicalizationMethod(); if (canonicalization != null) { bindingBuilder.canonicalizationMethod(canonicalization); } - bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signAssertions(); + bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signAssertions(); } - if (requiresEncryption(client)) { + if (samlClient.requiresEncryption()) { PublicKey publicKey = null; try { publicKey = SamlProtocolUtils.getEncryptionValidationKey(client); @@ -410,32 +394,6 @@ public class SamlProtocol implements LoginProtocol { } } - public static boolean requiresRealmSignature(ClientModel client) { - return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE)); - } - - public static boolean requiresAssertionSignature(ClientModel client) { - return "true".equals(client.getAttribute(SAML_ASSERTION_SIGNATURE)); - } - - public static boolean includeAuthnStatement(ClientModel client) { - return "true".equals(client.getAttribute(SAML_AUTHNSTATEMENT)); - } - - public static SignatureAlgorithm getSignatureAlgorithm(ClientModel client) { - String alg = client.getAttribute(SAML_SIGNATURE_ALGORITHM); - if (alg != null) { - SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); - if (algorithm != null) - return algorithm; - } - return SignatureAlgorithm.RSA_SHA256; - } - - private boolean requiresEncryption(ClientModel client) { - return "true".equals(client.getAttribute(SAML_ENCRYPT)); - } - public static class ProtocolMapperProcessor { final public T mapper; final public ProtocolMapperModel model; @@ -499,19 +457,20 @@ public class SamlProtocol implements LoginProtocol { @Override public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); + SamlClient samlClient = new SamlClient(client); if (!(client instanceof ClientModel)) return null; try { if (isLogoutPostBindingForClient(clientSession)) { String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING); SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client); - JaxrsSAML2BindingBuilder binding = createBindingBuilder(client); + JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient); return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri); } else { logger.debug("frontchannel redirect binding"); String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING); SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client); - JaxrsSAML2BindingBuilder binding = createBindingBuilder(client); + JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient); return binding.redirectBinding(logoutBuilder.buildDocument()).request(bindingUri); } } catch (ConfigurationException e) { @@ -574,6 +533,7 @@ public class SamlProtocol implements LoginProtocol { @Override public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); + SamlClient samlClient = new SamlClient(client); String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING); if (logoutUrl == null) { logger.warnv("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: {1}", client.getClientId()); @@ -583,7 +543,7 @@ public class SamlProtocol implements LoginProtocol { String logoutRequestString = null; try { - JaxrsSAML2BindingBuilder binding = createBindingBuilder(client); + JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient); logoutRequestString = binding.postBinding(logoutBuilder.buildDocument()).encoded(); } catch (Exception e) { logger.warn("failed to send saml logout", e); @@ -636,10 +596,10 @@ public class SamlProtocol implements LoginProtocol { return logoutBuilder; } - private JaxrsSAML2BindingBuilder createBindingBuilder(ClientModel client) { + private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) { JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(); - if (requiresRealmSignature(client)) { - binding.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); + if (samlClient.requiresRealmSignature()) { + binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } return binding; } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java index f7b4d1e97b..6ec5a1abd3 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java @@ -3,6 +3,7 @@ package org.keycloak.protocol.saml; import org.keycloak.Config; 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; @@ -14,6 +15,7 @@ import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java index d625f2a740..5742f7d1a3 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java @@ -25,7 +25,8 @@ public class SamlProtocolUtils { public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException { - if (!"true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { + SamlClient samlClient = new SamlClient(client); + if (!samlClient.requiresClientSignature()) { return; } PublicKey publicKey = getSignatureValidationKey(client); @@ -44,7 +45,7 @@ public class SamlProtocolUtils { } public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException { - return getPublicKey(client, SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE); + return getPublicKey(new SamlClient(client).getClientSigningCertificate()); } public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException { @@ -53,6 +54,10 @@ public class SamlProtocolUtils { public static PublicKey getPublicKey(ClientModel client, String attribute) throws VerificationException { String certPem = client.getAttribute(attribute); + return getPublicKey(certPem); + } + + private static PublicKey getPublicKey(String certPem) throws VerificationException { if (certPem == null) throw new VerificationException("Client does not have a public key."); Certificate cert = null; try { diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index f9aa30b148..30017145d5 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -193,6 +193,7 @@ public class SamlService extends AuthorizationEndpointBase { protected abstract SAMLDocumentHolder extractResponseDocument(String response); protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) { + SamlClient samlClient = new SamlClient(client); // validate destination if (requestAbstractType.getDestination() != null && !uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) { event.detail(Details.REASON, "invalid_destination"); @@ -200,7 +201,7 @@ public class SamlService extends AuthorizationEndpointBase { return ErrorPage.error(session, Messages.INVALID_REQUEST); } String bindingType = getBindingType(requestAbstractType); - if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) + if (samlClient.forcePostBinding()) bindingType = SamlProtocol.SAML_POST_BINDING; String redirect = null; URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); @@ -234,7 +235,7 @@ public class SamlService extends AuthorizationEndpointBase { // Handle NameIDPolicy from SP NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy(); - if (nameIdPolicy != null && !SamlProtocol.forceNameIdFormat(client)) { + if (nameIdPolicy != null && !samlClient.forceNameIDFormat()) { String nameIdFormat = nameIdPolicy.getFormat().toString(); // TODO: Handle AllowCreate too, relevant for persistent NameID. if (isSupportedNameIdFormat(nameIdFormat)) { @@ -274,6 +275,7 @@ public class SamlService extends AuthorizationEndpointBase { protected abstract String getBindingType(); protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) { + SamlClient samlClient = new SamlClient(client); // validate destination if (logoutRequest.getDestination() != null && !uriInfo.getAbsolutePath().equals(logoutRequest.getDestination())) { event.detail(Details.REASON, "invalid_destination"); @@ -285,20 +287,20 @@ public class SamlService extends AuthorizationEndpointBase { AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false); if (authResult != null) { String logoutBinding = getBindingType(); - if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) + if ("true".equals(samlClient.forcePostBinding())) logoutBinding = SamlProtocol.SAML_POST_BINDING; String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding); UserSessionModel userSession = authResult.getSession(); userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri); - if (SamlProtocol.requiresRealmSignature(client)) { - userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString()); + if (samlClient.requiresRealmSignature()) { + userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, samlClient.getSignatureAlgorithm().toString()); } if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState); userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID()); userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding); - userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE)); + userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod()); userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL); // remove client from logout requests for (ClientSessionModel clientSession : userSession.getClientSessions()) { @@ -348,8 +350,8 @@ public class SamlService extends AuthorizationEndpointBase { builder.destination(logoutBindingUri); builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString()); JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState); - if (SamlProtocol.requiresRealmSignature(client)) { - SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client); + if (samlClient.requiresRealmSignature()) { + SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm(); binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument(); } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java old mode 100644 new mode 100755 index dba1b295b9..81e05c2032 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java @@ -9,6 +9,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; +import org.keycloak.protocol.saml.SamlClient; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlProtocolFactory; import org.keycloak.protocol.saml.profile.ecp.util.Soap; @@ -69,8 +70,9 @@ public class SamlEcpProfileProtocolFactory extends SamlProtocolFactory { private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, SoapMessageBuilder messageBuilder) { ClientModel client = clientSession.getClient(); + SamlClient samlClient = new SamlClient(client); - if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { + if (samlClient.requiresClientSignature()) { SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP); ecpRequestAuthenticated.setMustUnderstand(true); From eddf3eef17ee76b6a36dc14a3ae50f19ad31515d Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 22 Dec 2015 14:44:43 +0100 Subject: [PATCH 30/65] KEYCLOAK-2242 Remove built-in admin account --- .../en/en-US/modules/server-installation.xml | 13 +- .../{resources/index.html => index.ftl} | 58 ++++++- .../exportimport/ExportImportManager.java | 141 +++++++++--------- .../services/managers/ApplianceBootstrap.java | 87 ++++++----- .../resources/KeycloakApplication.java | 68 +++++---- .../services/resources/WelcomeResource.java | 133 ++++++++++++++--- .../resources/admin/AdminConsole.java | 10 +- .../services/util/CacheControlUtil.java | 5 + .../testsuite/auth/page/WelcomePage.java | 44 ++++++ .../testsuite/AbstractKeycloakTest.java | 13 +- .../keycloak/testsuite/KeycloakServer.java | 27 ++-- 11 files changed, 397 insertions(+), 202 deletions(-) rename forms/common-themes/src/main/resources/theme/keycloak/welcome/{resources/index.html => index.ftl} (55%) create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/WelcomePage.java diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml index 01ad7e6c4a..4dc4b323b8 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml @@ -130,15 +130,14 @@ cd <WILDFLY_HOME>/bin
    Admin User - To access the admin console you need an account to login. Currently, there's a default account added - with the username admin and password admin. You will be required - to change the password on first login. We are planning on removing the built-in account soon and will - instead have an initial step to create the user. + To access the admin console to configure Keycloak you need an account to login. There is no built in user, + instead you have to first create an admin account. This can done either by opening http://localhost:8080/auth + (creating a user through the browser can only be done through localhost) or you can use the add-user script from + the command-line. - You can also create a user with the add-user script found in bin. - This script will create a temporary file with the details of the user, which are imported at startup. - To add a user with this script run: + The add-user script creates a temporary file with the details of the user, + which are imported at startup. To add a user with this script run: -p ]]> diff --git a/forms/common-themes/src/main/resources/theme/keycloak/welcome/resources/index.html b/forms/common-themes/src/main/resources/theme/keycloak/welcome/index.ftl similarity index 55% rename from forms/common-themes/src/main/resources/theme/keycloak/welcome/resources/index.html rename to forms/common-themes/src/main/resources/theme/keycloak/welcome/index.ftl index dd0b909bae..9a6c51c90a 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/welcome/resources/index.html +++ b/forms/common-themes/src/main/resources/theme/keycloak/welcome/index.ftl @@ -26,6 +26,28 @@ Welcome to Keycloak + @@ -36,7 +58,41 @@

    Welcome to Keycloak

    -

    Your Keycloak is running.

    + <#if successMessage?has_content> +

    ${successMessage}

    + <#elseif errorMessage?has_content> +

    ${errorMessage}

    + <#elseif bootstrap> + <#if localUser> +

    Please create an initial admin user to get started.

    + <#else> +

    + You need local access to create the initial admin user. Open http://localhost:8080/auth + or use the add-user script. +

    + + + + <#if bootstrap && localUser> +
    +

    + + +

    + +

    + + +

    + +

    + + +

    + + + +

    Documentation | Administration Console

    diff --git a/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java b/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java index cedcbddf50..4f94b52b79 100644 --- a/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java +++ b/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java @@ -2,10 +2,9 @@ package org.keycloak.exportimport; import org.jboss.logging.Logger; -import org.keycloak.Config; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.services.managers.ApplianceBootstrap; + +import java.io.IOException; /** * @author Marek Posolda @@ -14,76 +13,82 @@ public class ExportImportManager { private static final Logger logger = Logger.getLogger(ExportImportManager.class); - public void checkExportImport(KeycloakSessionFactory sessionFactory, String contextPath) { + private KeycloakSession session; + + private final String realmName; + + private ExportProvider exportProvider; + private ImportProvider importProvider; + + public ExportImportManager(KeycloakSession session) { + this.session = session; + + realmName = ExportImportConfig.getRealmName(); + + String providerId = ExportImportConfig.getProvider(); String exportImportAction = ExportImportConfig.getAction(); - String realmName = ExportImportConfig.getRealmName(); - boolean export = false; - boolean importt = false; if (ExportImportConfig.ACTION_EXPORT.equals(exportImportAction)) { - export = true; + exportProvider = session.getProvider(ExportProvider.class, providerId); + if (exportProvider == null) { + throw new RuntimeException("Export provider not found"); + } } else if (ExportImportConfig.ACTION_IMPORT.equals(exportImportAction)) { - importt = true; - } - - if (export || importt) { - String exportImportProviderId = ExportImportConfig.getProvider(); - logger.debug("Will use provider: " + exportImportProviderId); - KeycloakSession session = sessionFactory.create(); - - try { - if (export) { - ExportProvider exportProvider = session.getProvider(ExportProvider.class, exportImportProviderId); - - if (exportProvider == null) { - logger.errorf("Invalid Export Provider %s", exportImportProviderId); - } else { - if (realmName == null) { - logger.info("Full model export requested"); - exportProvider.exportModel(sessionFactory); - } else { - logger.infof("Export of realm '%s' requested", realmName); - exportProvider.exportRealm(sessionFactory, realmName); - } - logger.info("Export finished successfully"); - } - } else { - ImportProvider importProvider = session.getProvider(ImportProvider.class, exportImportProviderId); - - if (importProvider == null) { - logger.errorf("Invalid Import Provider %s", exportImportProviderId); - } else { - - Strategy strategy = ExportImportConfig.getStrategy(); - if (realmName == null) { - logger.infof("Full model import requested. Strategy: %s", strategy.toString()); - - // Check if master realm was exported. If it's not, then it needs to be created before other realms are imported - if (!importProvider.isMasterRealmExported()) { - ApplianceBootstrap.setupDefaultRealm(sessionFactory, contextPath); - ApplianceBootstrap.setupDefaultUser(sessionFactory); - } - - importProvider.importModel(sessionFactory, strategy); - } else { - logger.infof("Import of realm '%s' requested. Strategy: %s", realmName, strategy.toString()); - - if (!realmName.equals(Config.getAdminRealm())) { - // Check if master realm exists. If it's not, then it needs to be created before other realm is imported - ApplianceBootstrap.setupDefaultRealm(sessionFactory, contextPath); - ApplianceBootstrap.setupDefaultUser(sessionFactory); - } - - importProvider.importRealm(sessionFactory, realmName, strategy); - } - logger.info("Import finished successfully"); - } - } - } catch (Throwable ioe) { - logger.error("Error during export/import", ioe); - } finally { - session.close(); + importProvider = session.getProvider(ImportProvider.class, providerId); + if (importProvider == null) { + throw new RuntimeException("Import provider not found"); } } } + + public boolean isRunImport() { + return importProvider != null; + } + + public boolean isImportMasterIncluded() { + if (!isRunImport()) { + throw new IllegalStateException("Import not enabled"); + } + try { + return importProvider.isMasterRealmExported(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public boolean isRunExport() { + return exportProvider != null; + } + + public void runImport() { + try { + Strategy strategy = ExportImportConfig.getStrategy(); + if (realmName == null) { + logger.infof("Full model import requested. Strategy: %s", strategy.toString()); + importProvider.importModel(session.getKeycloakSessionFactory(), strategy); + } else { + logger.infof("Import of realm '%s' requested. Strategy: %s", realmName, strategy.toString()); + importProvider.importRealm(session.getKeycloakSessionFactory(), realmName, strategy); + } + logger.info("Import finished successfully"); + } catch (IOException e) { + throw new RuntimeException("Failed to run import", e); + } + } + + public void runExport() { + try { + if (realmName == null) { + logger.info("Full model export requested"); + exportProvider.exportModel(session.getKeycloakSessionFactory()); + } else { + logger.infof("Export of realm '%s' requested", realmName); + exportProvider.exportRealm(session.getKeycloakSessionFactory(), realmName); + } + logger.info("Export finished successfully"); + } catch (IOException e) { + throw new RuntimeException("Failed to run export"); + } + } + } diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 84020166d7..829dd6c9ac 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -4,15 +4,7 @@ import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.common.Version; import org.keycloak.common.enums.SslRequired; -import org.keycloak.models.AdminRoles; -import org.keycloak.models.ClientModel; -import org.keycloak.models.Constants; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; +import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.CredentialRepresentation; @@ -23,17 +15,34 @@ import org.keycloak.representations.idm.CredentialRepresentation; public class ApplianceBootstrap { private static final Logger logger = Logger.getLogger(ApplianceBootstrap.class); + private final KeycloakSession session; - public static boolean setupDefaultRealm(KeycloakSessionFactory sessionFactory, String contextPath) { - KeycloakSession session = sessionFactory.create(); - session.getTransaction().begin(); + public ApplianceBootstrap(KeycloakSession session) { + this.session = session; + } + public boolean isNewInstall() { + if (session.realms().getRealms().size() > 0) { + return false; + } else { + return true; + } + } + + public boolean isNoMasterUser() { + RealmModel realm = session.realms().getRealm(Config.getAdminRealm()); + return session.users().getUsersCount(realm) == 0; + } + + public boolean createMasterRealm(String contextPath) { + if (!isNewInstall()) { + throw new IllegalStateException("Can't create default realm as realms already exists"); + } + + KeycloakSession session = this.session.getKeycloakSessionFactory().create(); try { + session.getTransaction().begin(); String adminRealmName = Config.getAdminRealm(); - if (session.realms().getRealm(adminRealmName) != null) { - return false; - } - logger.info("Initializing " + adminRealmName + " realm"); RealmManager manager = new RealmManager(session); @@ -58,41 +67,29 @@ public class ApplianceBootstrap { KeycloakModelUtils.generateRealmKeys(realm); session.getTransaction().commit(); - return true; } finally { session.close(); } + + return true; } - public static boolean setupDefaultUser(KeycloakSessionFactory sessionFactory) { - KeycloakSession session = sessionFactory.create(); - session.getTransaction().begin(); - - try { - RealmModel realm = session.realms().getRealm(Config.getAdminRealm()); - if (session.users().getUserByUsername("admin", realm) == null) { - UserModel adminUser = session.users().addUser(realm, "admin"); - - adminUser.setEnabled(true); - UserCredentialModel usrCredModel = new UserCredentialModel(); - usrCredModel.setType(UserCredentialModel.PASSWORD); - usrCredModel.setValue("admin"); - session.users().updateCredential(realm, adminUser, usrCredModel); - adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - - RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); - adminUser.grantRole(adminRole); - - ClientModel accountApp = realm.getClientNameMap().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); - for (String r : accountApp.getDefaultRoles()) { - adminUser.grantRole(accountApp.getRole(r)); - } - } - session.getTransaction().commit(); - return true; - } finally { - session.close(); + public void createMasterRealmUser(KeycloakSession session, String username, String password) { + RealmModel realm = session.realms().getRealm(Config.getAdminRealm()); + if (session.users().getUsersCount(realm) > 0) { + throw new IllegalStateException("Can't create initial user as users already exists"); } + + UserModel adminUser = session.users().addUser(realm, username); + adminUser.setEnabled(true); + + UserCredentialModel usrCredModel = new UserCredentialModel(); + usrCredModel.setType(UserCredentialModel.PASSWORD); + usrCredModel.setValue(password); + session.users().updateCredential(realm, adminUser, usrCredModel); + + RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); + adminUser.grantRole(adminRole); } } 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 88569b6d2c..2d49464995 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -74,24 +74,51 @@ public class KeycloakApplication extends Application { classes.add(QRCodeResource.class); classes.add(ThemeResource.class); classes.add(JsResource.class); - classes.add(WelcomeResource.class); singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false")))); - boolean defaultRealmCreated = ApplianceBootstrap.setupDefaultRealm(sessionFactory, context.getContextPath()); - migrateModel(); sessionFactory.publish(new PostMigrationEvent()); - new ExportImportManager().checkExportImport(this.sessionFactory, context.getContextPath()); - importRealms(context); + boolean bootstrapAdminUser = false; - importAddUser(); + KeycloakSession session = sessionFactory.create(); + try { + session.getTransaction().begin(); - if (defaultRealmCreated) { - ApplianceBootstrap.setupDefaultUser(sessionFactory); + ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session); + ExportImportManager exportImportManager = new ExportImportManager(session); + + boolean createMasterRealm = applianceBootstrap.isNewInstall(); + if (exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded()) { + createMasterRealm = false; + } + + if (createMasterRealm) { + applianceBootstrap.createMasterRealm(contextPath); + } + + if (exportImportManager.isRunImport()) { + exportImportManager.runImport(); + } else { + importRealms(); + } + + importAddUser(); + + if (exportImportManager.isRunExport()) { + exportImportManager.runExport(); + } + + bootstrapAdminUser = applianceBootstrap.isNoMasterUser(); + + session.getTransaction().commit(); + } finally { + session.close(); } + singletons.add(new WelcomeResource(bootstrapAdminUser)); + setupScheduledTasks(sessionFactory); } @@ -185,34 +212,13 @@ public class KeycloakApplication extends Application { return singletons; } - public void importRealms(ServletContext context) { - importRealmFile(); - importRealmResources(context); - } - - public void importRealmResources(ServletContext context) { - String resources = context.getInitParameter("keycloak.import.realm.resources"); - if (resources != null) { - StringTokenizer tokenizer = new StringTokenizer(resources, ","); - while (tokenizer.hasMoreTokens()) { - String resource = tokenizer.nextToken().trim(); - InputStream is = context.getResourceAsStream(resource); - if (is == null) { - log.warn("Could not find realm resource to import: " + resource); - } - RealmRepresentation rep = loadJson(is, RealmRepresentation.class); - importRealm(rep, "resource " + resource); - } - } - } - - public void importRealmFile() { + public void importRealms() { String files = System.getProperty("keycloak.import"); if (files != null) { StringTokenizer tokenizer = new StringTokenizer(files, ","); while (tokenizer.hasMoreTokens()) { String file = tokenizer.nextToken().trim(); - RealmRepresentation rep = null; + RealmRepresentation rep; try { rep = loadJson(new FileInputStream(file), RealmRepresentation.class); } catch (FileNotFoundException e) { diff --git a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java index 7aa6b017bf..e778de3b00 100755 --- a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java @@ -2,22 +2,24 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.freemarker.FreeMarkerUtil; import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.ThemeProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.common.util.MimeTypeUtil; +import org.keycloak.services.managers.ApplianceBootstrap; +import org.keycloak.services.util.CacheControlUtil; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.CacheControl; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import javax.ws.rs.*; +import javax.ws.rs.core.*; +import java.io.IOException; import java.io.InputStream; +import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; /** * @author Stian Thorgersen @@ -27,12 +29,18 @@ public class WelcomeResource { private static final Logger logger = Logger.getLogger(WelcomeResource.class); + private boolean bootstrap; + @Context private UriInfo uriInfo; @Context protected KeycloakSession session; + public WelcomeResource(boolean bootstrap) { + this.bootstrap = bootstrap; + } + /** * Welcome page of Keycloak * @@ -42,11 +50,56 @@ public class WelcomeResource { @GET @Produces("text/html") public Response getWelcomePage() throws URISyntaxException { + checkBootstrap(); + String requestUri = uriInfo.getRequestUri().toString(); if (!requestUri.endsWith("/")) { return Response.seeOther(new URI(requestUri + "/")).build(); } else { - return getResource("index.html"); + return createWelcomePage(null, null); + } + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response createUser(final MultivaluedMap formData) { + checkBootstrap(); + + if (!bootstrap) { + return createWelcomePage(null, null); + } else { + if (!isLocal()) { + logger.errorv("Rejected non-local attempt to create initial user from {0}", session.getContext().getConnection().getRemoteAddr()); + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + String username = formData.getFirst("username"); + String password = formData.getFirst("password"); + String passwordConfirmation = formData.getFirst("passwordConfirmation"); + + if (username == null || username.length() == 0) { + return createWelcomePage(null, "Username is missing"); + } + + if (password == null || password.length() == 0) { + return createWelcomePage(null, "Password is missing"); + } + + if (!password.equals(passwordConfirmation)) { + return createWelcomePage(null, "Password and confirmation doesn't match"); + } + + ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session); + if (applianceBootstrap.isNoMasterUser()) { + bootstrap = false; + applianceBootstrap.createMasterRealmUser(session, username, password); + + logger.infov("Created initial admin user with username {0}", username); + return createWelcomePage("User created", null); + } else { + logger.warnv("Rejected attempt to create initial user as user is already created"); + return createWelcomePage(null, "Users already exists"); + } } } @@ -61,26 +114,62 @@ public class WelcomeResource { @Produces("text/html") public Response getResource(@PathParam("path") String path) { try { - Config.Scope config = Config.scope("theme"); - - ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); - Theme theme = themeProvider.getTheme(config.get("welcomeTheme"), Theme.Type.WELCOME); - InputStream resource = theme.getResourceAsStream(path); + InputStream resource = getTheme().getResourceAsStream(path); if (resource != null) { String contentType = MimeTypeUtil.getContentType(path); - - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoTransform(false); - cacheControl.setMaxAge(config.getInt("staticMaxAge", -1)); - - Response.ResponseBuilder builder = Response.ok(resource).type(contentType).cacheControl(cacheControl); + Response.ResponseBuilder builder = Response.ok(resource).type(contentType).cacheControl(CacheControlUtil.getDefaultCacheControl()); return builder.build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); } + } catch (IOException e) { + throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + + private Response createWelcomePage(String successMessage, String errorMessage) { + try { + Map map = new HashMap<>(); + map.put("bootstrap", bootstrap); + if (bootstrap) { + map.put("localUser", isLocal()); + } + if (successMessage != null) { + map.put("successMessage", successMessage); + } + if (errorMessage != null) { + map.put("errorMessage", errorMessage); + } + FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); + String result = freeMarkerUtil.processTemplate(map, "index.ftl", getTheme()); + return Response.status(errorMessage == null ? Response.Status.OK : Response.Status.BAD_REQUEST).entity(result).cacheControl(CacheControlUtil.noCache()).build(); } catch (Exception e) { - logger.warn("Failed to get theme resource", e); - return Response.serverError().build(); + throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + + private Theme getTheme() { + Config.Scope config = Config.scope("theme"); + ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); + try { + return themeProvider.getTheme(config.get("welcomeTheme"), Theme.Type.WELCOME); + } catch (IOException e) { + throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + + private void checkBootstrap() { + if (bootstrap) { + bootstrap = new ApplianceBootstrap(session).isNoMasterUser(); + } + } + + private boolean isLocal() { + try { + InetAddress inetAddress = InetAddress.getByName(session.getContext().getConnection().getRemoteAddr()); + return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress(); + } catch (UnknownHostException e) { + throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index 5c22200a41..50e33ddc96 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -264,10 +264,7 @@ public class AdminConsole { if (!uriInfo.getRequestUri().getPath().endsWith("/")) { return Response.status(302).location(uriInfo.getRequestUriBuilder().path("/").build()).build(); } else { - String adminTheme = realm.getAdminTheme(); - if (adminTheme == null) { - adminTheme = "keycloak"; - } + Theme theme = getTheme(); Map map = new HashMap<>(); @@ -277,11 +274,8 @@ public class AdminConsole { authUrl = authUrl.substring(0, authUrl.length() - 1); map.put("authUrl", authUrl); - map.put("resourceUrl", Urls.themeRoot(baseUri) + "/admin/" + adminTheme); + map.put("resourceUrl", Urls.themeRoot(baseUri) + "/admin/" + theme.getName()); map.put("resourceVersion", Version.RESOURCES_VERSION); - - Theme theme = getTheme(); - map.put("properties", theme.getProperties()); FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); diff --git a/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java b/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java index 259083a2d4..0e2f1e0c97 100644 --- a/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java +++ b/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java @@ -19,7 +19,12 @@ public class CacheControlUtil { cacheControl.setNoCache(true); } return cacheControl; + } + public static CacheControl noCache() { + CacheControl cacheControl = new CacheControl(); + cacheControl.setNoCache(true); + return cacheControl; } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/WelcomePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/WelcomePage.java new file mode 100644 index 0000000000..3adbbac92e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/WelcomePage.java @@ -0,0 +1,44 @@ +package org.keycloak.testsuite.auth.page; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Stian Thorgersen + */ +public class WelcomePage extends AuthServer { + + @FindBy(id = "username") + private WebElement usernameInput; + + @FindBy(id = "password") + private WebElement passwordInput; + + @FindBy(id = "passwordConfirmation") + private WebElement passwordConfirmationInput; + + @FindBy(id = "create-button") + private WebElement createButton; + + public boolean isPasswordSet() { + return !driver.getPageSource().contains("Please create an initial admin user to get started."); + } + + public void setPassword(String username, String password) { + usernameInput.clear(); + usernameInput.sendKeys(username); + + passwordInput.clear(); + passwordInput.sendKeys(password); + + passwordConfirmationInput.clear(); + passwordConfirmationInput.sendKeys(password); + + createButton.click(); + + if (!driver.getPageSource().contains("User created")) { + throw new RuntimeException("Failed to updated password"); + } + } + +} 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 063347a7ea..13ec96e4d6 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 @@ -20,6 +20,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import static org.keycloak.testsuite.admin.Users.setPasswordFor; import org.keycloak.testsuite.arquillian.SuiteContext; +import org.keycloak.testsuite.auth.page.WelcomePage; import org.keycloak.testsuite.util.OAuthClient; import org.openqa.selenium.WebDriver; import org.keycloak.testsuite.auth.page.AuthServer; @@ -76,6 +77,9 @@ public abstract class AbstractKeycloakTest { @Page protected UpdatePassword updatePasswordPage; + @Page + protected WelcomePage welcomePage; + protected UserRepresentation adminUser; @Before @@ -103,11 +107,10 @@ public abstract class AbstractKeycloakTest { } private void updateMasterAdminPassword() { - accountPage.navigateTo(); - loginPage.form().login(ADMIN, ADMIN); - updatePasswordPage.updatePasswords(ADMIN, ADMIN); - assertCurrentUrlStartsWith(accountPage); - deleteAllCookiesForMasterRealm(); + welcomePage.navigateTo(); + if (!welcomePage.isPasswordSet()) { + welcomePage.setPassword("admin", "admin"); + } } public void deleteAllCookiesForMasterRealm() { 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 28f0915189..2466f3621d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java @@ -33,10 +33,10 @@ import org.jboss.resteasy.spi.ResteasyDeployment; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.filters.ClientConnectionFilter; import org.keycloak.services.filters.KeycloakSessionServletFilter; +import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.util.JsonSerialization; @@ -273,19 +273,17 @@ public class KeycloakServer { } protected void setupDevConfig() { - KeycloakSession session = sessionFactory.create(); - session.getTransaction().begin(); - - try { - RealmManager manager = new RealmManager(session); - - RealmModel adminRealm = manager.getKeycloakAdminstrationRealm(); - UserModel admin = session.users().getUserByUsername("admin", adminRealm); - admin.removeRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - - session.getTransaction().commit(); - } finally { - session.close(); + if (System.getProperty("keycloak.createAdminUser", "true").equals("true")) { + KeycloakSession session = sessionFactory.create(); + try { + session.getTransaction().begin(); + if (new ApplianceBootstrap(session).isNoMasterUser()) { + new ApplianceBootstrap(session).createMasterRealmUser(session, "admin", "admin"); + } + session.getTransaction().commit(); + } finally { + session.close(); + } } } @@ -311,7 +309,6 @@ public class KeycloakServer { di.setDefaultEncoding("UTF-8"); di.setDefaultServletConfig(new DefaultServletConfig(true)); - di.addWelcomePage("theme/keycloak/welcome/resources/index.html"); FilterInfo filter = Servlets.filter("SessionFilter", KeycloakSessionServletFilter.class); di.addFilter(filter); From b24dfa00094c7ab1f73acfcb6011bc0c671407d7 Mon Sep 17 00:00:00 2001 From: Jean Merelis Date: Sat, 2 Jan 2016 00:37:47 -0200 Subject: [PATCH 31/65] translation of messages to pt_BR --- .../messages/messages_pt_BR.properties | 7 +++- .../login/messages/messages_pt_BR.properties | 33 ++++++++++++++----- .../email/messages/messages_pt_BR.properties | 5 ++- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties index 37c0106632..161ae60b52 100644 --- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties @@ -52,8 +52,10 @@ role_manage-events=Gerencia eventos role_view-profile=Visualiza perfil role_manage-account=Gerencia conta role_read-token=L\u00EA token +role_offline-access=Acesso Offline client_account=Conta client_security-admin-console=Console de Administra\u00E7\u00E3o de Seguran\u00E7a +client_admin-cli=Admin CLI client_realm-management=Gerenciamento de Realm client_broker=Broker @@ -85,9 +87,11 @@ application=Aplicativo availablePermissions=Permiss\u00F5es Dispon\u00EDveis grantedPermissions=Permiss\u00F5es Concedidas grantedPersonalInfo=Informa\u00E7\u00F5es Pessoais Concedidas +additionalGrants=Concess\u00F5es Adicionais action=A\u00E7\u00E3o inResource=em fullAccess=Acesso Completo +offlineToken=Offline Token revoke=Revogar Concess\u00F5es configureAuthenticators=Autenticadores Configurados @@ -130,6 +134,7 @@ federatedIdentityLinkNotActiveMessage=Esta identidade n\u00E3o est\u00E1 mais em federatedIdentityRemovingLastProviderMessage=Voc\u00EA n\u00E3o pode remover a \u00FAltima identidade federada como voc\u00EA n\u00E3o tem senha identityProviderRedirectErrorMessage=Falha ao redirecionar para o provedor de identidade identityProviderRemovedMessage=Provedor de identidade removido com sucesso +identityProviderAlreadyLinkedMessage=Identidade federada retornado por {0} j\u00E1 est\u00E1 ligado a outro usu\u00E1rio. accountDisabledMessage=Conta desativada, contate o administrador @@ -147,4 +152,4 @@ locale_de=Deutsch locale_en=English locale_it=Italian locale_pt-BR=Portugu\u00EAs (BR) -locale_fr=Fran\u00e7ais \ No newline at end of file +locale_fr=Fran\u00E7ais \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties index a1441202bd..2dbbaf0510 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -1,4 +1,3 @@ -doLogIn=Entrar doRegister=Cadastre-se doCancel=Cancelar doSubmit=Ok @@ -15,9 +14,9 @@ kerberosNotConfiguredTitle=Kerberos N\u00E3o Configurado bypassKerberosDetail=Ou voc\u00EA n\u00E3o est\u00E1 logado via Kerberos ou o seu navegador n\u00E3o est\u00E1 configurado para login Kerberos. Por favor, clique em continuar para fazer o login no atrav\u00E9s de outros meios kerberosNotSetUp=Kerberos n\u00E3o est\u00E1 configurado. Voc\u00EA n\u00E3o pode acessar. registerWithTitle=Registre-se com {0} -registerWithTitleHtml={0} +registerWithTitleHtml=Registre-se com {0} loginTitle=Entrar em {0} -loginTitleHtml={0} +loginTitleHtml=Entrar em {0} impersonateTitle={0} Impersonate User impersonateTitleHtml={0} Impersonate User realmChoice=Realm @@ -26,7 +25,7 @@ loginTotpTitle=Configura\u00E7\u00E3o do autenticador mobile loginProfileTitle=Atualiza\u00E7\u00E3o das Informa\u00E7\u00F5es da Conta loginTimeout=Voc\u00EA demorou muito para entrar. Por favor, refa\u00E7a o processo de login a partir do in\u00EDcio. oauthGrantTitle=Concess\u00E3o OAuth -oauthGrantTitleHtml={0} +oauthGrantTitleHtml=Acesso tempor\u00E1rio para {0} solicitado pela errorTitle=N\u00F3s lamentamos... errorTitleHtml=N\u00F3s lamentamos ... emailVerifyTitle=Verifica\u00E7\u00E3o de e-mail @@ -79,6 +78,11 @@ emailVerifyInstruction1=Um e-mail com instru\u00E7\u00F5es para verificar o seu emailVerifyInstruction2=Voc\u00EA n\u00E3o recebeu um c\u00F3digo de verifica\u00E7\u00E3o em seu e-mail? emailVerifyInstruction3=para reenviar o e-mail. +emailLinkIdpTitle=Vincular {0} +emailLinkIdp1=Um email com instru\u00E7\u00F5es para vincular a conta {0} {1} com sua conta {2} foi enviado para voc\u00EA. +emailLinkIdp2=N\u00E3o recebeu um c\u00F3digo de verifica\u00E7\u00E3o no e-mail? +emailLinkIdp3=para reenviar o email. + backToLogin=« Voltar emailInstruction=Digite seu nome de usu\u00E1rio ou endere\u00E7o de email e n\u00F3s lhe enviaremos instru\u00E7\u00F5es sobre como criar uma nova senha. @@ -89,6 +93,7 @@ personalInfo=Informa\u00E7\u00F5es Pessoais: role_admin=Admininstrador role_realm-admin=Administra Realm role_create-realm=Cria realm +role_create-client=Cria cliente role_view-realm=Visualiza realm role_view-users=Visualiza usu\u00E1rios role_view-applications=Visualiza aplicativos @@ -104,8 +109,10 @@ role_manage-events=Gerencia eventos role_view-profile=Visualiza perfil role_manage-account=Gerencia contas role_read-token=L\u00EA token +role_offline-access=Acesso offline client_account=Conta client_security-admin-console=Console de Administra\u00E7\u00E3o de Seguran\u00E7a +client_admin-cli=Admin CLI client_realm-management=Gerenciamento de Realm client_broker=Broker @@ -130,13 +137,19 @@ invalidTotpMessage=C\u00F3digo autenticador inv\u00E1lido. usernameExistsMessage=Nome de usu\u00E1rio j\u00E1 existe. emailExistsMessage=Email j\u00E1 existe. -federatedIdentityEmailExistsMessage=J\u00E1 existe usu\u00E1rio com este email. Por favor acesse sua conta de gest\u00E3o para vincular a conta. -federatedIdentityUsernameExistsMessage=J\u00E1 existe usu\u00E1rio com este nome de usu\u00E1rio. Por favor acessar sua conta de gest\u00E3o para vincular a conta. +federatedIdentityExistsMessage=Usu\u00E1rio com {0} {1} j\u00E1 existe. Por favor, entre em gerenciamento de contas para vincular a conta. + +confirmLinkIdpTitle=Conta j\u00E1 existente +federatedIdentityConfirmLinkMessage=Usu\u00E1rio com {0} {1} j\u00E1 existe. Como voc\u00EA quer continuar? +federatedIdentityConfirmReauthenticateMessage=Autenticar como {0} para vincular sua conta com {1} +confirmLinkIdpReviewProfile=Revisar informa\u00E7\u00F5es do perfil +confirmLinkIdpContinue=Vincular {0} com uma conta existente configureTotpMessage=Voc\u00EA precisa configurar seu celular com o autenticador Mobile para ativar sua conta. updateProfileMessage=Voc\u00EA precisa atualizar o seu perfil de usu\u00E1rio para ativar sua conta. updatePasswordMessage=Voc\u00EA precisa mudar sua senha para ativar sua conta. verifyEmailMessage=Voc\u00EA precisa verificar o seu endere\u00E7o de e-mail para ativar sua conta. +linkIdpMessage=Voc\u00EA precisa confirmar o seu endere\u00E7o de e-mail para vincular sua conta com {0}. emailSentMessage=Voc\u00EA dever\u00E1 receber um e-mail em breve com mais instru\u00E7\u00F5es. emailSendErrorMessage=Falha ao enviar e-mail, por favor, tente novamente mais tarde @@ -163,7 +176,8 @@ failedLogout=Falha ao sair unknownLoginRequesterMessage=Solicitante de login desconhecido loginRequesterNotEnabledMessage=Solicitante de login desativado bearerOnlyMessage=Aplicativos somente ao portador n\u00E3o tem permiss\u00E3o para iniciar o login pelo navegador -directGrantsOnlyMessage=Clientes de concess\u00E3o direta n\u00E3o tem permiss\u00E3o para iniciar o login pelo navegador +standardFlowDisabledMessage=Cliente n\u00E3o tem permiss\u00E3o para iniciar o login com response_type informado. O fluxo padr\u00E3o est\u00E1 desabilitado para o cliente. +implicitFlowDisabledMessage=Cliente n\u00E3o tem permiss\u00E3o para iniciar o login com response_type informado. O fluxo padr\u00E3o est\u00E1 desabilitado para o cliente. invalidRedirectUriMessage=URI de redirecionamento inv\u00E1lido unsupportedNameIdFormatMessage=NameIDFormat n\u00E3o suportado invlidRequesterMessage=Solicitante inv\u00E1lido @@ -172,13 +186,13 @@ resetCredentialNotAllowedMessage=Reset Credential not allowed permissionNotApprovedMessage=Permiss\u00E3o n\u00E3o aprovada. noRelayStateInResponseMessage=Sem estado de retransmiss\u00E3o na resposta do provedor de identidade. -identityProviderAlreadyLinkedMessage=A identidade retornado pelo provedor de identidade j\u00E1 est\u00E1 vinculado a outro usu\u00E1rio. insufficientPermissionMessage=Permiss\u00F5es insuficientes para vincular identidades. couldNotProceedWithAuthenticationRequestMessage=N\u00E3o foi poss\u00EDvel proceder \u00E0 solicita\u00E7\u00E3o de autentica\u00E7\u00E3o para provedor de identidade. couldNotObtainTokenMessage=N\u00E3o foi poss\u00EDvel obter token do provedor de identidade. unexpectedErrorRetrievingTokenMessage=Erro inesperado ao recuperar token do provedor de identidade. unexpectedErrorHandlingResponseMessage=Erro inesperado ao manusear resposta do provedor de identidade. identityProviderAuthenticationFailedMessage=Falha na autentica\u00E7\u00E3o. N\u00E3o foi poss\u00EDvel autenticar com o provedor de identidade. +identityProviderDifferentUserMessage=Autenticado como {0}, mas era esperado ser autenticado como {1} couldNotSendAuthenticationRequestMessage=N\u00E3o foi poss\u00EDvel enviar solicita\u00E7\u00E3o de autentica\u00E7\u00E3o para o provedor de identidade. unexpectedErrorHandlingRequestMessage=Erro inesperado ao manusear pedido de autentica\u00E7\u00E3o para provedor de identidade. invalidAccessCodeMessage=C\u00F3digo de acesso inv\u00E1lido. @@ -186,6 +200,7 @@ sessionNotActiveMessage=Sess\u00E3o inativa. invalidCodeMessage=C\u00F3digo inv\u00E1lido, por favor fa\u00E7a login novamente atrav\u00E9s de sua aplica\u00E7\u00E3o. identityProviderUnexpectedErrorMessage=Erro inesperado durante a autentica\u00E7\u00E3o com o provedor de identidade identityProviderNotFoundMessage=N\u00E3o foi poss\u00EDvel encontrar um provedor de identidade com o identificador. +identityProviderLinkSuccess=Sua conta foi vinculada com sucesso com {0} conta {1} . realmSupportsNoCredentialsMessage=O realm n\u00E3o suporta qualquer tipo de credencial. identityProviderNotUniqueMessage=O realm suporta m\u00FAltiplos provedores de identidade. N\u00E3o foi poss\u00EDvel determinar qual o provedor de identidade deve ser usado para se autenticar. emailVerifiedMessage=O seu endere\u00E7o de e-mail foi confirmado. @@ -194,7 +209,7 @@ locale_de=Deutsch locale_en=English locale_it=Italian locale_pt-BR=Portugu\u00EAs (BR) -locale_fr=Fran\u00e7ais +locale_fr=Fran\u00E7ais locale_es=Espa\u00F1ol backToApplication=« Voltar para o aplicativo diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties index 06b4647a4c..6596fba771 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties @@ -1,6 +1,9 @@ emailVerificationSubject=Verifica\u00E7\u00E3o de e-mail emailVerificationBody=Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email\n\n{0}\n\nEste link ir\u00E1 expirar dentro de {1} minutos.\n\nSe n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem. emailVerificationBodyHtml=

    Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email

    {0}

    Este link ir\u00E1 expirar dentro de {1} minutos.

    Se n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.

    +identityProviderLinkSubject=Vincular {0} +identityProviderLinkBody=Algu\u00E9m quer vincular sua conta "{1}" com a conta "{0}" do usu\u00E1rio {2} . Se foi voc\u00EA, clique no link abaixo para vincular as contas.\n\n{3}\n\nEste link ir\u00E1 expirar em {4} minutos.\n\nSe voc\u00EA n\u00E3o quer vincular a conta, apenas ignore esta mensagem. Se voc\u00EA vincular as contas, voc\u00EA ser\u00E1 capaz de logar em {1} atr\u00E1v\u00E9s de {0}. +identityProviderLinkBodyHtml=

    Algu\u00E9m quer vincular sua conta {1} com a conta {0} do usu\u00E1rio {2} . Se foi voc\u00EA, clique no link abaixo para vincular as contas.

    {3}

    Este link ir\u00E1 expirar em {4} minutos.

    Se voc\u00EA n\u00E3o quer vincular a conta, apenas ignore esta mensagem. Se voc\u00EA vincular as contas, voc\u00EA ser\u00E1 capaz de logar em {1} atr\u00E1v\u00E9s de {0}.

    passwordResetSubject=Redefini\u00E7\u00E3o de senha 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=

    Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.

    {0}

    This link will expire within {1} minutes.

    If you don''t want to reset your credentials, just ignore this message and nothing will be changed.

    @@ -18,4 +21,4 @@ eventUpdatePasswordBody=Sua senha foi alterada em {0} de {1}. Se n\u00E3o foi vo eventUpdatePasswordBodyHtml=

    Sua senha foi alterada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.

    eventUpdateTotpSubject=Atualiza\u00E7\u00E3o TOTP eventUpdateTotpBody=TOTP foi atualizado para a sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador. -eventUpdateTotpBodyHtml=

    TOTP foi atualizado para a sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.

    +eventUpdateTotpBodyHtml=

    TOTP foi atualizado para a sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.

    \ No newline at end of file From c5f19ff2b347bf62712aa27c9d3df41ba3b915d4 Mon Sep 17 00:00:00 2001 From: Jean Merelis Date: Sat, 2 Jan 2016 23:23:04 -0200 Subject: [PATCH 32/65] fix some missing translation (pt_BR) --- .../theme/base/login/messages/messages_pt_BR.properties | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties index 2dbbaf0510..d60bb8e10e 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -1,3 +1,4 @@ +doLogin=Entrar doRegister=Cadastre-se doCancel=Cancelar doSubmit=Ok @@ -38,9 +39,9 @@ termsTitle=Termos e Condi\u00E7\u00F5es termsTitleHtml=Termos e Condi\u00E7\u00F5es termsText=

    Termos e Condi\u00E7\u00F5es a ser definido

    -recaptchaFailed=Invalid Recaptcha -recaptchaNotConfigured=Recaptcha is required, but not configured -consentDenied=Consent denied. +recaptchaFailed=Recaptcha inv\u00E1lido +recaptchaNotConfigured=Recaptcha \u00E9 requerido, mas n\u00E3o foi configurado +consentDenied=Consentimento negado. noAccount=Novo usu\u00E1rio? username=Nome de usu\u00E1rio @@ -182,7 +183,7 @@ invalidRedirectUriMessage=URI de redirecionamento inv\u00E1lido unsupportedNameIdFormatMessage=NameIDFormat n\u00E3o suportado invlidRequesterMessage=Solicitante inv\u00E1lido registrationNotAllowedMessage=Registro n\u00E3o permitido. -resetCredentialNotAllowedMessage=Reset Credential not allowed +resetCredentialNotAllowedMessage=N\u00E3o \u00E9 permitido redefinir credencial. permissionNotApprovedMessage=Permiss\u00E3o n\u00E3o aprovada. noRelayStateInResponseMessage=Sem estado de retransmiss\u00E3o na resposta do provedor de identidade. From 2fce7f849897cbc8278457e05fcf562958e0864c Mon Sep 17 00:00:00 2001 From: Jean Merelis Date: Sun, 3 Jan 2016 21:02:32 -0200 Subject: [PATCH 33/65] fix doLogIn key (pt_BR) --- .../theme/base/login/messages/messages_pt_BR.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties index d60bb8e10e..58c91c848e 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -1,4 +1,4 @@ -doLogin=Entrar +doLogIn=Entrar doRegister=Cadastre-se doCancel=Cancelar doSubmit=Ok From 392ce0b323b389b572ba34a7f030d56e605d2287 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 4 Jan 2016 09:21:46 +0100 Subject: [PATCH 34/65] KEYCLOAK-2250 Infinispan offline sessions cache is missing from configuration files --- ...DefaultInfinispanConnectionProviderFactory.java | 14 +------------- .../src/main/xslt/standalone-ha.xsl | 1 + .../src/main/xslt/standalone.xsl | 1 + .../KeycloakServerDeploymentProcessor.java | 1 + .../subsystem-templates/keycloak-infinispan.xml | 2 ++ 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index d1e7ac6999..be76cc913c 100755 --- a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -66,19 +66,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon } else { initEmbedded(); } - - // Backwards compatibility - if (cacheManager.getCacheConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) == null) { - logger.debugf("No configuration provided for '%s' cache. Using '%s' configuration as template", - InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, InfinispanConnectionProvider.SESSION_CACHE_NAME); - - Configuration sessionCacheConfig = cacheManager.getCacheConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME); - if (sessionCacheConfig != null) { - ConfigurationBuilder confBuilder = new ConfigurationBuilder().read(sessionCacheConfig); - Configuration offlineSessionConfig = confBuilder.build(); - cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionConfig); - } - } } } } @@ -139,6 +126,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon Configuration sessionCacheConfiguration = sessionConfigBuilder.build(); cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration); + cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration); cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration); } diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone-ha.xsl b/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone-ha.xsl index 31f681fd77..c0680719c5 100755 --- a/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone-ha.xsl +++ b/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone-ha.xsl @@ -43,6 +43,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone.xsl b/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone.xsl index 3d4b77cedb..403fe346d6 100755 --- a/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone.xsl +++ b/distribution/server-overlay/eap6/eap6-server-overlay/src/main/xslt/standalone.xsl @@ -51,6 +51,7 @@ + diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java index 150dbbd1d5..1c255e91e0 100644 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java @@ -60,6 +60,7 @@ public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcesso st.addDependency(cacheContainerService.append("realms")); st.addDependency(cacheContainerService.append("users")); st.addDependency(cacheContainerService.append("sessions")); + st.addDependency(cacheContainerService.append("offlineSessions")); st.addDependency(cacheContainerService.append("loginFailures")); } } diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index 1d92afc6d5..5da36aa969 100644 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -11,6 +11,7 @@ + @@ -59,6 +60,7 @@ + From f48753f6e1ba9db4ca2b5b70b9a060474ae7765c Mon Sep 17 00:00:00 2001 From: Vaclav Muzikar Date: Mon, 4 Jan 2016 12:36:38 +0100 Subject: [PATCH 35/65] Enable LdapUserFederationTest#invalidSettingsTest - already fixed --- .../testsuite/console/federation/LdapUserFederationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java index 7dc7ab2de5..be504117e4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java @@ -82,7 +82,6 @@ public class LdapUserFederationTest extends AbstractConsoleTest { } @Test - @Ignore public void invalidSettingsTest() { createLdapUserProvider.navigateTo(); createLdapUserProvider.form().selectVendor(ACTIVE_DIRECTORY); From eece3689f20b276aa282b4b92af7448e4ed6c039 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 4 Jan 2016 13:04:33 +0100 Subject: [PATCH 36/65] KEYCLOAK-2228 Deleting of realm when using JPA requires server restart --- .../InfinispanCacheRealmProviderFactory.java | 15 +++++++++++++-- .../cache/infinispan/InfinispanRealmCache.java | 6 ++++++ .../org/keycloak/models/cache/RealmCache.java | 2 ++ .../resources/admin/RealmAdminResource.java | 2 -- .../org/keycloak/testsuite/admin/RealmTest.java | 11 +++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java index 5a76153793..5aec527ac8 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java @@ -17,9 +17,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.CacheRealmProviderFactory; -import org.keycloak.models.cache.RealmCache; +import org.keycloak.models.cache.entities.CachedClient; import org.keycloak.models.cache.entities.CachedRealm; -import org.keycloak.models.cache.entities.CachedUser; import java.util.concurrent.ConcurrentHashMap; @@ -143,11 +142,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa realmLookup.remove(realm.getName()); + for (String r : realm.getRealmRoles().values()) { + realmCache.evictCachedRoleById(r); + } + for (String c : realm.getClients().values()) { realmCache.evictCachedApplicationById(c); } log.tracev("Realm removed realm={0}", realm.getName()); + } else if (object instanceof CachedClient) { + CachedClient client = (CachedClient) object; + + for (String r : client.getRoles().values()) { + realmCache.evictCachedRoleById(r); + } + + log.tracev("Client removed client={0}", client.getId()); } } } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java index 4cd4f79641..07e2d9af7e 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java @@ -160,6 +160,12 @@ public class InfinispanRealmCache implements RealmCache { cache.remove(id); } + @Override + public void evictCachedRoleById(String id) { + logger.tracev("Evicting role {0}", id); + cache.evict(id); + } + @Override public void addCachedRole(CachedRole role) { if (!enabled) return; diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java index e826ac1715..56f9bc087b 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java @@ -37,6 +37,8 @@ public interface RealmCache { void invalidateRole(CachedRole role); + void evictCachedRoleById(String id); + void addCachedRole(CachedRole role); void invalidateCachedRoleById(String id); 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 038f43b568..9d144e4c2d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -264,8 +264,6 @@ public class RealmAdminResource { if (!new RealmManager(session).removeRealm(realm)) { throw new NotFoundException("Realm doesn't exist"); - } else { - clearAdminEvents(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java index 604dcffca4..b2fb326c5d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -3,6 +3,9 @@ package org.keycloak.testsuite.admin; import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.Test; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ServerInfoResource; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.ClientRepresentation; @@ -90,6 +93,14 @@ public class RealmTest extends AbstractClientTest { assertNames(keycloak.realms().findAll(), "master", "test"); } + @Test + public void loginAfterRemoveRealm() { + realm.remove(); + + ServerInfoResource serverInfoResource = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID).serverInfo(); + serverInfoResource.getInfo(); + } + @Test public void updateRealm() { // first change From e7009cac477d2c4a503ccc057f2ef53937e4df7e Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 4 Jan 2016 13:15:18 +0100 Subject: [PATCH 37/65] KEYCLOAK-2204 Add test to make sure disabled client can't refresh token --- .../testsuite/oauth/RefreshTokenTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index a69b733ab6..b8002d3f67 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -331,6 +331,48 @@ public class RefreshTokenTest { } } + @Test + public void refreshTokenClientDisabled() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + Event loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = response.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + events.expectCodeToToken(codeId, sessionId).assertEvent(); + + try { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getClientByClientId(oauth.getClientId()).setEnabled(false); + } + }); + + response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_client", response.getError()); + + events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).session((String) null).clearDetails().error(Errors.CLIENT_DISABLED).assertEvent(); + } finally { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getClientByClientId(oauth.getClientId()).setEnabled(true); + } + }); + + } + } + @Test public void refreshTokenUserSessionExpired() { oauth.doLogin("test-user@localhost", "password"); From cea70337ef9b3ac65be47e6b7ecc4c30deb5b959 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 4 Jan 2016 15:49:57 -0200 Subject: [PATCH 38/65] Tooltips for initial access token fields --- .../theme/base/admin/messages/admin-messages_en.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 1fc4e75ed0..0cd4accc7b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -492,7 +492,9 @@ client.description.tooltip=Specifies description of the client. For example 'My expires=Expires expiration=Expiration +expiration.tooltip=Specifies how long the token should be valid count=Count +count.tooltip=Specifies how many clients can be created using the token remainingCount=Remaining count created=Created back=Back From 1b857387f26cc7daac15f02f0be11a495fc85d0b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 4 Jan 2016 15:09:12 -0200 Subject: [PATCH 39/65] Minor change for client registration documentation --- .../reference/en/en-US/modules/client-registration.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml index fab7119e4f..35f73a3e19 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml @@ -193,8 +193,8 @@ String initialAccessToken = "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLT ClientRepresentation client = new ClientRepresentation(); client.setClientId(CLIENT_ID); -ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm").build(); -reg.auth(initialAccessToken); +ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm/clients").build(); +reg.auth(Auth.token(initialAccessToken)); client = reg.create(client); From 57971ce0b2670b13c189212edf52c3ed842044eb Mon Sep 17 00:00:00 2001 From: Thomas Raehalme Date: Mon, 4 Jan 2016 21:09:36 +0200 Subject: [PATCH 40/65] Changed docs to match the new code. --- .../en/en-US/modules/spring-security-adapter.xml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml b/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml index 33c2aa287d..0d3c20c0df 100644 --- a/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml @@ -115,7 +115,10 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter - + + + + @@ -124,7 +127,7 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter - + @@ -157,6 +160,15 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter +
    + Multi Tenancy + + The Keycloak Spring Security adapter also supports multi tenancy. Instead of injecting + AdapterDeploymentContextFactoryBean with the path to keycloak.json you + can inject an implementation of the KeycloakConfigResolver interface. More details on how + to implement the KeycloakConfigResolver can be found in . + +
    Naming Security Roles From 8356409b44258656e588f146948661d74e7b6828 Mon Sep 17 00:00:00 2001 From: Thomas Raehalme Date: Mon, 4 Jan 2016 21:16:37 +0200 Subject: [PATCH 41/65] Fixed some typos in the docs and added paragraph regarding GrantedAuthoritiesMapper. --- .../en/en-US/modules/spring-security-adapter.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml b/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml index 0d3c20c0df..dce3d3d899 100644 --- a/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/spring-security-adapter.xml @@ -1,7 +1,7 @@
    Spring Security Adapter - To to secure an application with Spring Security and Keyloak, add this adapter as a dependency to your project. + To secure an application with Spring Security and Keycloak, add this adapter as a dependency to your project. You then have to provide some extra beans in your Spring Security configuration file and add the Keycloak security filter to your pipeline. @@ -176,6 +176,14 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter For example, an administrator role must be declared in Keycloak as ROLE_ADMIN or similar, not simply ADMIN. + + The class org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider + supports an optional org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper + which can be used to map roles coming from Keycloak to roles recognized by Spring Security. Use, for example, + org.springframework.security.core.authority.mapping.SimpleAuthorityMapper to insert the + ROLE_ prefix and convert the role name to upper case. The class is part of Spring Security + Core module. +
    Client to Client Support From d8d0298498f31c70aeac352180fe3660c5167209 Mon Sep 17 00:00:00 2001 From: Dane Barentine Date: Mon, 4 Jan 2016 11:30:57 -0800 Subject: [PATCH 42/65] KEYCLOAK-2255 Location header should return IdP alias instead of provider ID. --- .../services/resources/admin/IdentityProvidersResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index bf452ff0f1..84bbdad573 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -170,7 +170,7 @@ public class IdentityProvidersResource { adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, identityProvider.getInternalId()) .representation(representation).success(); - return Response.created(uriInfo.getAbsolutePathBuilder().path(representation.getProviderId()).build()).build(); + return Response.created(uriInfo.getAbsolutePathBuilder().path(representation.getAlias()).build()).build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Identity Provider " + representation.getAlias() + " already exists"); } From 3ec516d14cb48663f2dee613500cb9b860e5f3db Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 4 Jan 2016 22:23:14 +0100 Subject: [PATCH 43/65] KEYCLOAK-2253 - Add support for ConfiguredProvider based UserFederationProviderFactory. UserFederationProvidersResource is now aware of ConfiguredProvider and allows sophisticated configuration of configuration properties via ProviderConfigProperty definitions. See DummyUserFederationProviderFactory. getConfigProperties() for example. Previously UserFederationProvidersResource did only support simple key-value pairs for expressing configurable options. Tested this by launching a standalone KeycloakServer and creating a new Dummy UserFederationProvider. The default values, labels and help messages are correctly displayed and the values are stored correctly. --- .../admin/resources/js/controllers/users.js | 10 +++ .../resources/partials/federated-generic.html | 3 + .../UserFederationProvidersResource.java | 76 ++++++++++++++++++- .../DummyUserFederationProviderFactory.java | 33 +++++++- 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index e72c8eca3d..56c5b21421 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -644,6 +644,16 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif instance.config.updateProfileFirstLogin = true; instance.config.allowKerberosAuthentication = true; } + + if (providerFactory.properties) { + + for (var i = 0; i < providerFactory.properties.length; i++) { + var configProperty = providerFactory.properties[i]; + var configValue = configProperty.type == "boolean" ? (configProperty.defaultValue === true ? 'true' : 'false') : configProperty.defaultValue; + instance.config[configProperty.name] = configValue; + } + } + } else { $scope.fullSyncEnabled = (instance.fullSyncPeriod && instance.fullSyncPeriod > 0); $scope.changedSyncEnabled = (instance.changedSyncPeriod && instance.changedSyncPeriod > 0); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html index b74a9abc08..df34f728c5 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html @@ -37,6 +37,9 @@
    + + +
    diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java index e0cbe730b4..1c390ad5ac 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java @@ -13,7 +13,10 @@ import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.provider.ConfiguredProvider; +import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; import org.keycloak.representations.idm.UserFederationProviderRepresentation; @@ -32,6 +35,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -119,10 +123,22 @@ public class UserFederationProvidersResource { if (!factory.getId().equals(id)) { continue; } + + if (factory instanceof ConfiguredProvider) { + + UserFederationProviderFactoryDescription rep = new UserFederationProviderFactoryDescription(); + rep.setId(factory.getId()); + + ConfiguredProvider cp = (ConfiguredProvider) factory; + rep.setHelpText(cp.getHelpText()); + rep.setProperties(toConfigPropertyRepresentationList(cp.getConfigProperties())); + + return rep; + } + UserFederationProviderFactoryRepresentation rep = new UserFederationProviderFactoryRepresentation(); rep.setId(factory.getId()); - rep.setOptions(((UserFederationProviderFactory)factory).getConfigurationOptions()); - + rep.setOptions(((UserFederationProviderFactory) factory).getConfigurationOptions()); return rep; } @@ -191,4 +207,60 @@ public class UserFederationProvidersResource { return instanceResource; } + + private ConfigPropertyRepresentation toConfigPropertyRepresentation(ProviderConfigProperty prop) { + + ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation(); + propRep.setName(prop.getName()); + propRep.setLabel(prop.getLabel()); + propRep.setType(prop.getType()); + propRep.setDefaultValue(prop.getDefaultValue()); + propRep.setHelpText(prop.getHelpText()); + + return propRep; + } + + private List toConfigPropertyRepresentationList(List props) { + + List reps = new ArrayList<>(props.size()); + for(ProviderConfigProperty prop : props){ + reps.add(toConfigPropertyRepresentation(prop)); + } + + return reps; + } + + + public static class UserFederationProviderFactoryDescription extends UserFederationProviderFactoryRepresentation { + + protected String name; + + protected String helpText; + + protected List properties; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHelpText() { + return helpText; + } + + public void setHelpText(String helpText) { + this.helpText = helpText; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java index 9825f5b19a..82715c912a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java @@ -8,17 +8,17 @@ import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.provider.ConfiguredProvider; +import org.keycloak.provider.ProviderConfigProperty; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class DummyUserFederationProviderFactory implements UserFederationProviderFactory { +public class DummyUserFederationProviderFactory implements UserFederationProviderFactory, ConfiguredProvider { private static final Logger logger = Logger.getLogger(DummyUserFederationProviderFactory.class); public static final String PROVIDER_NAME = "dummy"; @@ -84,4 +84,29 @@ public class DummyUserFederationProviderFactory implements UserFederationProvide public int getChangedSyncCounter() { return changedSyncCounter.get(); } + + @Override + public String getHelpText() { + return "Dummy User Federation Provider Help Text"; + } + + @Override + public List getConfigProperties() { + + ProviderConfigProperty prop1 = new ProviderConfigProperty(); + prop1.setName("prop1"); + prop1.setLabel("Prop1"); + prop1.setDefaultValue("prop1Default"); + prop1.setHelpText("Prop1 HelpText"); + prop1.setType(ProviderConfigProperty.STRING_TYPE); + + ProviderConfigProperty prop2 = new ProviderConfigProperty(); + prop2.setName("prop2"); + prop2.setLabel("Prop2"); + prop2.setDefaultValue(true); + prop2.setHelpText("Prop2 HelpText"); + prop2.setType(ProviderConfigProperty.BOOLEAN_TYPE); + + return Arrays.asList(prop1, prop2); + } } From edcc39d906407b4f894d878c6470b36bf5cafe1d Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 4 Jan 2016 23:07:08 +0100 Subject: [PATCH 44/65] KEYCLOAK-2253 - Use string representation for boolean properties in GenericUserFederationCtrl. Adapted due to code review. --- .../theme/base/admin/resources/js/controllers/users.js | 3 +-- .../keycloak/testsuite/DummyUserFederationProviderFactory.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 56c5b21421..2f3dc48a41 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -649,8 +649,7 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif for (var i = 0; i < providerFactory.properties.length; i++) { var configProperty = providerFactory.properties[i]; - var configValue = configProperty.type == "boolean" ? (configProperty.defaultValue === true ? 'true' : 'false') : configProperty.defaultValue; - instance.config[configProperty.name] = configValue; + instance.config[configProperty.name] = configProperty.defaultValue; } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java index 82715c912a..fc8b8d6227 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java @@ -103,7 +103,7 @@ public class DummyUserFederationProviderFactory implements UserFederationProvide ProviderConfigProperty prop2 = new ProviderConfigProperty(); prop2.setName("prop2"); prop2.setLabel("Prop2"); - prop2.setDefaultValue(true); + prop2.setDefaultValue("true"); prop2.setHelpText("Prop2 HelpText"); prop2.setType(ProviderConfigProperty.BOOLEAN_TYPE); From 3bacbdf6ff5007c5aca0bb4719202e71741b7514 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Mon, 4 Jan 2016 17:13:15 -0500 Subject: [PATCH 45/65] set framework for template config --- .../idm/ClientTemplateRepresentation.java | 82 +++++++++++ .../keycloak/models/ClientConfigResolver.java | 55 ++++++++ .../models/utils/RepresentationToModel.java | 36 ++++- .../keycloak/protocol/saml/SamlClient.java | 36 ++--- .../saml/SamlClientRepresentation.java | 59 -------- .../protocol/saml/SamlClientTemplate.java | 131 ++++++++++++++++++ .../protocol/saml/SamlProtocolFactory.java | 46 +++++- .../saml/SamlRepresentationAttributes.java | 65 +++++++++ .../keycloak/protocol/saml/SamlService.java | 3 +- .../profile/ecp/SamlEcpProfileService.java | 3 +- .../protocol/LoginProtocolFactory.java | 12 +- .../oidc/OIDCLoginProtocolFactory.java | 7 + 12 files changed, 449 insertions(+), 86 deletions(-) create mode 100755 model/api/src/main/java/org/keycloak/models/ClientConfigResolver.java delete mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java mode change 100644 => 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java index dc575c4109..e478c27600 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java @@ -17,6 +17,16 @@ public class ClientTemplateRepresentation { protected String description; protected String protocol; protected Boolean fullScopeAllowed; + protected Boolean bearerOnly; + protected Boolean consentRequired; + protected Boolean standardFlowEnabled; + protected Boolean implicitFlowEnabled; + protected Boolean directAccessGrantsEnabled; + protected Boolean serviceAccountsEnabled; + protected Boolean publicClient; + protected Boolean frontchannelLogout; + protected Map attributes; + protected List protocolMappers; public String getId() { @@ -67,4 +77,76 @@ public class ClientTemplateRepresentation { public void setFullScopeAllowed(Boolean fullScopeAllowed) { this.fullScopeAllowed = fullScopeAllowed; } + + public Boolean isBearerOnly() { + return bearerOnly; + } + + public void setBearerOnly(Boolean bearerOnly) { + this.bearerOnly = bearerOnly; + } + + public Boolean isConsentRequired() { + return consentRequired; + } + + public void setConsentRequired(Boolean consentRequired) { + this.consentRequired = consentRequired; + } + + public Boolean isStandardFlowEnabled() { + return standardFlowEnabled; + } + + public void setStandardFlowEnabled(Boolean standardFlowEnabled) { + this.standardFlowEnabled = standardFlowEnabled; + } + + public Boolean isImplicitFlowEnabled() { + return implicitFlowEnabled; + } + + public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) { + this.implicitFlowEnabled = implicitFlowEnabled; + } + + public Boolean isDirectAccessGrantsEnabled() { + return directAccessGrantsEnabled; + } + + public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) { + this.directAccessGrantsEnabled = directAccessGrantsEnabled; + } + + public Boolean isServiceAccountsEnabled() { + return serviceAccountsEnabled; + } + + public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) { + this.serviceAccountsEnabled = serviceAccountsEnabled; + } + + public Boolean isPublicClient() { + return publicClient; + } + + public void setPublicClient(Boolean publicClient) { + this.publicClient = publicClient; + } + + public Boolean isFrontchannelLogout() { + return frontchannelLogout; + } + + public void setFrontchannelLogout(Boolean frontchannelLogout) { + this.frontchannelLogout = frontchannelLogout; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } } diff --git a/model/api/src/main/java/org/keycloak/models/ClientConfigResolver.java b/model/api/src/main/java/org/keycloak/models/ClientConfigResolver.java new file mode 100755 index 0000000000..8ec8728da7 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ClientConfigResolver.java @@ -0,0 +1,55 @@ +package org.keycloak.models; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientConfigResolver { + protected ClientModel client; + protected ClientTemplateModel clientTemplate; + + public ClientConfigResolver(ClientModel client) { + this.client = client; + this.clientTemplate = client.getClientTemplate(); + } + + public String resolveAttribute(String name) { + if (clientTemplate != null && client.useTemplateConfig()) { + return clientTemplate.getAttribute(name); + } else { + return client.getAttribute(name); + } + } + + public boolean isFrontchannelLogout() { + if (clientTemplate != null && client.useTemplateConfig()) { + return clientTemplate.isFrontchannelLogout(); + } + + return client.isFrontchannelLogout(); + } + + boolean isConsentRequired() { + if (clientTemplate != null && client.useTemplateConfig()) { + return clientTemplate.isConsentRequired(); + } + + return client.isConsentRequired(); + } + + boolean isStandardFlowEnabled() { + if (clientTemplate != null && client.useTemplateConfig()) { + return clientTemplate.isStandardFlowEnabled(); + } + + return client.isStandardFlowEnabled(); + } + + boolean isServiceAccountsEnabled() { + if (clientTemplate != null && client.useTemplateConfig()) { + return clientTemplate.isServiceAccountsEnabled(); + } + + return client.isServiceAccountsEnabled(); + } +} diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 2432fc1274..3b2cfdc5db 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -903,7 +903,7 @@ public class RepresentationToModel { } } if (resourceRep.isUseTemplateConfig() != null) client.setUseTemplateConfig(resourceRep.isUseTemplateConfig()); - else client.setUseTemplateConfig(resourceRep.getClientTemplate() != null); + else client.setUseTemplateConfig(false); // default to false for now if (resourceRep.isUseTemplateScope() != null) client.setUseTemplateScope(resourceRep.isUseTemplateScope()); else client.setUseTemplateScope(resourceRep.getClientTemplate() != null); @@ -1022,6 +1022,23 @@ public class RepresentationToModel { client.addProtocolMapper(toModel(mapper)); } } + if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly()); + if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired()); + + if (resourceRep.isStandardFlowEnabled() != null) client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled()); + if (resourceRep.isImplicitFlowEnabled() != null) client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled()); + if (resourceRep.isDirectAccessGrantsEnabled() != null) client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled()); + if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled()); + + if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient()); + if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); + + if (resourceRep.getAttributes() != null) { + for (Map.Entry entry : resourceRep.getAttributes().entrySet()) { + client.setAttribute(entry.getKey(), entry.getValue()); + } + } + return client; } @@ -1035,6 +1052,23 @@ public class RepresentationToModel { if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); + + if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly()); + if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired()); + if (rep.isStandardFlowEnabled() != null) resource.setStandardFlowEnabled(rep.isStandardFlowEnabled()); + if (rep.isImplicitFlowEnabled() != null) resource.setImplicitFlowEnabled(rep.isImplicitFlowEnabled()); + if (rep.isDirectAccessGrantsEnabled() != null) resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled()); + if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled()); + if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient()); + if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed()); + if (rep.isFrontchannelLogout() != null) resource.setFrontchannelLogout(rep.isFrontchannelLogout()); + + if (rep.getAttributes() != null) { + for (Map.Entry entry : rep.getAttributes().entrySet()) { + resource.setAttribute(entry.getKey(), entry.getValue()); + } + } + } public static long getClaimsMask(ClaimRepresentation rep) { diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java index df27de2fbc..d935f83e2a 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java @@ -1,5 +1,6 @@ package org.keycloak.protocol.saml; +import org.keycloak.models.ClientConfigResolver; import org.keycloak.models.ClientModel; import org.keycloak.saml.SignatureAlgorithm; @@ -7,24 +8,14 @@ import org.keycloak.saml.SignatureAlgorithm; * @author Bill Burke * @version $Revision: 1 $ */ -public class SamlClient { - protected ClientModel client; +public class SamlClient extends ClientConfigResolver { public SamlClient(ClientModel client) { - this.client = client; + super(client); } - public String getId() { - return client.getId(); - } - - public String getClientId() { - return client.getClientId(); - } -// - public String getCanonicalizationMethod() { - return client.getAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + return resolveAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); } public void setCanonicalizationMethod(String value) { @@ -32,7 +23,7 @@ public class SamlClient { } public SignatureAlgorithm getSignatureAlgorithm() { - String alg = client.getAttribute(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM); + String alg = resolveAttribute(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM); if (alg != null) { SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); if (algorithm != null) @@ -46,14 +37,14 @@ public class SamlClient { } public String getNameIDFormat() { - return client.getAttributes().get(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); + return resolveAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); } public void setNameIDFormat(String format) { client.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, format); } public boolean includeAuthnStatement() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT)); } public void setIncludeAuthnStatement(boolean val) { @@ -61,7 +52,7 @@ public class SamlClient { } public boolean forceNameIDFormat() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); } public void setForceNameIDFormat(boolean val) { @@ -69,7 +60,7 @@ public class SamlClient { } public boolean requiresRealmSignature() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE)); } public void setRequiresRealmSignature(boolean val) { @@ -78,7 +69,7 @@ public class SamlClient { } public boolean forcePostBinding() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING)); } public void setForcePostBinding(boolean val) { @@ -86,7 +77,7 @@ public class SamlClient { } public boolean requiresAssertionSignature() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); } public void setRequiresAssertionSignature(boolean val) { @@ -94,7 +85,7 @@ public class SamlClient { } public boolean requiresEncryption() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_ENCRYPT)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ENCRYPT)); } @@ -104,7 +95,7 @@ public class SamlClient { } public boolean requiresClientSignature() { - return "true".equals(client.getAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE)); + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE)); } public void setRequiresClientSignature(boolean val) { @@ -129,4 +120,5 @@ public class SamlClient { client.setAttribute(SamlConfigAttributes.SAML_SIGNING_PRIVATE_KEY, val); } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java deleted file mode 100755 index 0ce4beba43..0000000000 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientRepresentation.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.keycloak.protocol.saml; - -import org.keycloak.representations.idm.ClientRepresentation; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SamlClientRepresentation { - protected ClientRepresentation rep; - - public SamlClientRepresentation(ClientRepresentation rep) { - this.rep = rep; - } - - public String getCanonicalizationMethod() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); - } - - public String getSignatureAlgorithm() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM); - } - - public String getNameIDFormat() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); - - } - - public String getIncludeAuthnStatement() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_AUTHNSTATEMENT); - - } - - public String getForceNameIDFormat() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE); - } - - public String getSamlServerSignature() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE); - - } - - public String getForcePostBinding() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_FORCE_POST_BINDING); - - } - public String getClientSignature() { - if (rep.getAttributes() == null) return null; - return rep.getAttributes().get(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE); - - } -} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java new file mode 100755 index 0000000000..ae6d491795 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java @@ -0,0 +1,131 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.saml.SignatureAlgorithm; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlClientTemplate { + protected ClientTemplateModel clientTemplate; + + public SamlClientTemplate(ClientTemplateModel template) { + this.clientTemplate = template; + } + + public String getId() { + return clientTemplate.getId(); + } + +// + + public String getCanonicalizationMethod() { + return clientTemplate.getAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + } + + public void setCanonicalizationMethod(String value) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE, value); + } + + public SignatureAlgorithm getSignatureAlgorithm() { + String alg = null; + alg = clientTemplate.getAttribute(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + if (alg != null) { + SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg); + if (algorithm != null) + return algorithm; + } + return SignatureAlgorithm.RSA_SHA256; + } + + public void setSignatureAlgorithm(SignatureAlgorithm algorithm) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, algorithm.name()); + } + + public String getNameIDFormat() { + return clientTemplate.getAttributes().get(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); + } + public void setNameIDFormat(String format) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, format); + } + + public boolean includeAuthnStatement() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT)); + } + + public void setIncludeAuthnStatement(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, Boolean.toString(val)); + } + + public boolean forceNameIDFormat() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE)); + + } + public void setForceNameIDFormat(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val)); + } + + public boolean requiresRealmSignature() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE)); + } + + public void setRequiresRealmSignature(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val)); + + } + + public boolean forcePostBinding() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING)); + } + + public void setForcePostBinding(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING, Boolean.toString(val)); + + } + public boolean requiresAssertionSignature() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE)); + } + + public void setRequiresAssertionSignature(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE , Boolean.toString(val)); + + } + public boolean requiresEncryption() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_ENCRYPT)); + } + + + public void setRequiresEncryption(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, Boolean.toString(val)); + + } + + public boolean requiresClientSignature() { + return "true".equals(clientTemplate.getAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE)); + } + + public void setRequiresClientSignature(boolean val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE , Boolean.toString(val)); + + } + + public String getClientSigningCertificate() { + return clientTemplate.getAttribute(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE); + } + + public void setClientSigningCertificate(String val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, val); + + } + + public String getClientSigningPrivateKey() { + return clientTemplate.getAttribute(SamlConfigAttributes.SAML_SIGNING_PRIVATE_KEY); + } + + public void setClientSigningPrivateKey(String val) { + clientTemplate.setAttribute(SamlConfigAttributes.SAML_SIGNING_PRIVATE_KEY, val); + + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java index 6ec5a1abd3..1de9f456aa 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java @@ -105,7 +105,7 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { @Override public void setupClientDefaults(ClientRepresentation clientRep, ClientModel newClient) { - SamlClientRepresentation rep = new SamlClientRepresentation(clientRep); + SamlRepresentationAttributes rep = new SamlRepresentationAttributes(clientRep.getAttributes()); SamlClient client = new SamlClient(newClient); if (clientRep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true); if (rep.getCanonicalizationMethod() == null) { @@ -136,9 +136,53 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { if (rep.getClientSignature() == null) { client.setRequiresClientSignature(true); + } + + if (client.requiresClientSignature() && client.getClientSigningCertificate() == null) { CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(newClient.getClientId()); client.setClientSigningCertificate(info.getCertificate()); client.setClientSigningPrivateKey(info.getPrivateKey()); + + } + + if (clientRep.isFrontchannelLogout() == null) { + newClient.setFrontchannelLogout(true); + } + } + + @Override + public void setupTemplateDefaults(ClientTemplateRepresentation clientRep, ClientTemplateModel newClient) { + SamlRepresentationAttributes rep = new SamlRepresentationAttributes(clientRep.getAttributes()); + SamlClientTemplate client = new SamlClientTemplate(newClient); + if (clientRep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true); + if (rep.getCanonicalizationMethod() == null) { + client.setCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE); + } + if (rep.getSignatureAlgorithm() == null) { + client.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256); + } + + if (rep.getNameIDFormat() == null) { + client.setNameIDFormat("username"); + } + + if (rep.getIncludeAuthnStatement() == null) { + client.setIncludeAuthnStatement(true); + } + + if (rep.getForceNameIDFormat() == null) { + client.setForceNameIDFormat(false); + } + + if (rep.getSamlServerSignature() == null) { + client.setRequiresRealmSignature(true); + } + if (rep.getForcePostBinding() == null) { + client.setForcePostBinding(true); + } + + if (rep.getClientSignature() == null) { + client.setRequiresClientSignature(true); } if (clientRep.isFrontchannelLogout() == null) { diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java new file mode 100755 index 0000000000..5933773d3d --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java @@ -0,0 +1,65 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.representations.idm.ClientRepresentation; + +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlRepresentationAttributes { + protected Map attributes; + + public SamlRepresentationAttributes(Map attributes) { + this.attributes = attributes; + } + + public String getCanonicalizationMethod() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_CANONICALIZATION_METHOD_ATTRIBUTE); + } + + protected Map getAttributes() { + return attributes; + } + + public String getSignatureAlgorithm() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM); + } + + public String getNameIDFormat() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); + + } + + public String getIncludeAuthnStatement() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_AUTHNSTATEMENT); + + } + + public String getForceNameIDFormat() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE); + } + + public String getSamlServerSignature() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE); + + } + + public String getForcePostBinding() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_FORCE_POST_BINDING); + + } + public String getClientSignature() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE); + + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index a09dd390cd..bd6e84693f 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -412,7 +412,8 @@ public class SamlService extends AuthorizationEndpointBase { @Override protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException { - if (!"true".equals(client.getAttribute("saml.client.signature"))) { + SamlClient samlClient = new SamlClient(client); + if (!samlClient.requiresClientSignature()) { return; } PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client); diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java old mode 100644 new mode 100755 index 52b855702b..f2623464b9 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java @@ -9,6 +9,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; +import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlService; import org.keycloak.protocol.saml.profile.ecp.util.Soap; @@ -99,7 +100,7 @@ public class SamlEcpProfileService extends SamlService { private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, Soap.SoapMessageBuilder messageBuilder) { ClientModel client = clientSession.getClient(); - if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { + if ("true".equals(client.getAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP); ecpRequestAuthenticated.setMustUnderstand(true); diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java index a876b194b5..2238e6f21f 100755 --- a/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/LoginProtocolFactory.java @@ -2,10 +2,12 @@ package org.keycloak.protocol; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.services.managers.AuthenticationManager; import java.util.List; @@ -31,10 +33,18 @@ public interface LoginProtocolFactory extends ProviderFactory { Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager); /** - * Setup default values for new clients. + * Setup default values for new clients. This expects that the representation has already set up the client * * @param rep * @param newClient */ void setupClientDefaults(ClientRepresentation rep, ClientModel newClient); + + /** + * Setup default values for new templates. This expects that the representation has already set up the template + * + * @param clientRep + * @param newClient + */ + void setupTemplateDefaults(ClientTemplateRepresentation clientRep, ClientTemplateModel newClient); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index 8cd0d8fed5..ee710cec48 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -21,6 +21,7 @@ import org.keycloak.common.constants.KerberosConstants; import org.keycloak.common.util.UriUtils; 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; @@ -32,6 +33,7 @@ import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.services.managers.AuthenticationManager; import java.util.ArrayList; @@ -206,4 +208,9 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { if (rep.isPublicClient() == null) newClient.setPublicClient(true); if (rep.isFrontchannelLogout() == null) newClient.setFrontchannelLogout(false); } + + @Override + public void setupTemplateDefaults(ClientTemplateRepresentation clientRep, ClientTemplateModel newClient) { + + } } From 15506e3db395b079ccb355d5a6ded799d71ff634 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 5 Jan 2016 00:04:40 +0100 Subject: [PATCH 46/65] KEYCLOAK-2256 - Guarantee deterministic ordering for custom user attributes in admin console. We now iterate over custom-user attributes in a deterministic way such that new attributes don't lead to a complete reordering of the properties in the custom user attributes listings. Previously the order of custom attributes in the user attributes listings was not deterministic which lead to cases where keys that shared a common prefix where not placed at arbitrary positions. Introduced a custom angularjs flter "toOrderedMapSortedByKey" that helps to deterministically iterate over object properites. --- .../theme/base/admin/resources/js/app.js | 28 +++++++++++++++++++ .../resources/partials/user-attributes.html | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index e5e1bb919b..65693881f9 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -2372,6 +2372,34 @@ module.filter('capitalize', function() { }; }); +/* + * Guarantees a deterministic property iteration order. + * See: http://www.2ality.com/2015/10/property-traversal-order-es6.html + */ +module.filter('toOrderedMapSortedByKey', function(){ + return function(input){ + + if(!input){ + return input; + } + + var keys = Object.keys(input); + + if(keys.length <= 1){ + return input; + } + + keys.sort(); + + var result = {}; + for (var i = 0; i < keys.length; i++) { + result[keys[i]] = input[keys[i]]; + } + + return result; + }; +}); + module.directive('kcSidebarResize', function ($window) { return function (scope, element) { function resize() { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html index df969fb1ca..31dde8a775 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html @@ -16,7 +16,7 @@
    - +
    {{key}} From b6718b44a121a87ae5edb5184ff308dcffcbd5c5 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 4 Jan 2016 23:25:40 +0100 Subject: [PATCH 47/65] KEYCLOAK-2178 KEYCLOAK-1744 Added MSADUserAccountControlMapper. Removing enableUserAccountControlAfterPasswordUpdate option --- .../keycloak/federation/ldap/LDAPConfig.java | 6 - .../ldap/LDAPFederationProvider.java | 19 +- .../ldap/LDAPFederationProviderFactory.java | 7 + .../federation/ldap/idm/model/LDAPObject.java | 4 + .../ldap/idm/query/internal/LDAPQuery.java | 4 + .../ldap/idm/store/IdentityStore.java | 5 +- .../idm/store/ldap/LDAPIdentityStore.java | 18 +- .../idm/store/ldap/LDAPOperationManager.java | 17 +- .../mappers/AbstractLDAPFederationMapper.java | 6 + .../ldap/mappers/LDAPFederationMapper.java | 15 ++ .../mappers/LDAPFederationMapperBridge.java | 9 +- .../msad/MSADUserAccountControlMapper.java | 249 ++++++++++++++++++ .../MSADUserAccountControlMapperFactory.java | 68 +++++ .../ldap/mappers/msad/UserAccountControl.java | 58 ++++ ...ycloak.mappers.UserFederationMapperFactory | 3 +- .../resources/partials/federated-ldap.html | 8 - .../keycloak/migration/MigrationModel.java | 4 +- .../migration/MigrationModelManager.java | 7 + .../migration/migrators/MigrateTo1_8_0.java | 48 ++++ .../org/keycloak/models/LDAPConstants.java | 9 +- .../testsuite/util/LDAPTestConfiguration.java | 2 - .../ldap/LDAPTestConfiguration.java | 2 - .../FederationProvidersIntegrationTest.java | 2 +- 23 files changed, 516 insertions(+), 54 deletions(-) create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java create mode 100644 model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_8_0.java diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java index 18c2d8fc3d..e8af5a9dd8 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java @@ -113,12 +113,6 @@ public class LDAPConfig { return uuidAttrName; } - // TODO: Remove and use mapper instead? - public boolean isUserAccountControlsAfterPasswordUpdate() { - String userAccountCtrls = config.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE); - return userAccountCtrls==null ? false : Boolean.parseBoolean(userAccountCtrls); - } - public boolean isPagination() { String pagination = config.get(LDAPConstants.PAGINATION); return pagination==null ? false : Boolean.parseBoolean(pagination); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 698a392c12..f93de4633b 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -41,6 +41,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.naming.AuthenticationException; + /** * @author Marek Posolda * @author Bill Burke @@ -366,7 +368,22 @@ public class LDAPFederationProvider implements UserFederationProvider { } else { // Use Naming LDAP API LDAPObject ldapUser = loadAndValidateUser(realm, user); - return ldapIdentityStore.validatePassword(ldapUser, password); + + try { + ldapIdentityStore.validatePassword(ldapUser, password); + return true; + } catch (AuthenticationException ae) { + + // Check if any mapper provides callback for handle LDAP AuthenticationException + Set federationMappers = realm.getUserFederationMappersByFederationProvider(getModel().getId()); + boolean processed = false; + for (UserFederationMapperModel mapperModel : federationMappers) { + LDAPFederationMapper ldapMapper = getMapper(mapperModel); + processed = processed || ldapMapper.onAuthenticationFailure(mapperModel, this, ldapUser, user, ae, realm); + } + + return processed; + } } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 551d940acd..539b9c7b02 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -16,6 +16,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory; import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -188,6 +189,12 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); realm.addUserFederationMapper(mapperModel); + + // MSAD specific mapper for account state propagation + if (activeDirectory) { + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("MSAD account controls", newProviderModel.getId(), MSADUserAccountControlMapperFactory.PROVIDER_ID); + realm.addUserFederationMapper(mapperModel); + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java index cbb28f96ed..a334ed0264 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java @@ -66,6 +66,10 @@ public class LDAPObject { readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase()); } + public void removeReadOnlyAttributeName(String readOnlyAttribute) { + readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase()); + } + public String getRdnAttributeName() { return rdnAttributeName; } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java index 6939697db2..3019942206 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java @@ -189,4 +189,8 @@ public class LDAPQuery { return this.conditions; } + public LDAPFederationProvider getLdapProvider() { + return ldapFedProvider; + } + } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java index 23c6d99eb5..77691b10b0 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java @@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.idm.store; import java.util.List; +import javax.naming.AuthenticationException; + import org.keycloak.federation.ldap.LDAPConfig; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; @@ -65,8 +67,9 @@ public interface IdentityStore { * * @param user Keycloak user * @param password Ldap password + * @throws AuthenticationException if authentication is not successful */ - boolean validatePassword(LDAPObject user, String password); + void validatePassword(LDAPObject user, String password) throws AuthenticationException; /** * Updates the specified credential value. diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java index 8193679be4..e217fe4e03 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -11,6 +11,7 @@ import java.util.NoSuchElementException; import java.util.Set; import java.util.TreeSet; +import javax.naming.AuthenticationException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; @@ -173,18 +174,14 @@ public class LDAPIdentityStore implements IdentityStore { // *************** CREDENTIALS AND USER SPECIFIC STUFF @Override - public boolean validatePassword(LDAPObject user, String password) { + public void validatePassword(LDAPObject user, String password) throws AuthenticationException { String userDN = user.getDn().toString(); if (logger.isTraceEnabled()) { logger.tracef("Using DN [%s] for authentication of user", userDN); } - if (operationManager.authenticate(userDN, password)) { - return true; - } - - return false; + operationManager.authenticate(userDN, password); } @Override @@ -225,15 +222,6 @@ public class LDAPIdentityStore implements IdentityStore { List modItems = new ArrayList(); modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd)); - // Used in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored - // TODO: Remove and use mapper instead - if (getConfig().isUserAccountControlsAfterPasswordUpdate()) { - BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512"); - modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl)); - - logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN); - } - operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {})); } catch (Exception e) { throw new ModelException(e); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java index 8a5b299437..3d0d22bd76 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import javax.naming.AuthenticationException; import javax.naming.Binding; import javax.naming.Context; import javax.naming.InitialContext; @@ -320,15 +321,15 @@ public class LDAPOperationManager { * * @param dn * @param password + * @throws AuthenticationException if authentication is not successful * - * @return */ - public boolean authenticate(String dn, String password) { + public void authenticate(String dn, String password) throws AuthenticationException { InitialContext authCtx = null; try { if (password == null || password.isEmpty()) { - throw new Exception("Empty password used"); + throw new AuthenticationException("Empty password used"); } Hashtable env = new Hashtable(this.connectionProperties); @@ -342,13 +343,15 @@ public class LDAPOperationManager { authCtx = new InitialLdapContext(env, null); - return true; - } catch (Exception e) { + } catch (AuthenticationException ae) { if (logger.isDebugEnabled()) { - logger.debugf(e, "Authentication failed for DN [%s]", dn); + logger.debugf(ae, "Authentication failed for DN [%s]", dn); } - return false; + throw ae; + } catch (Exception e) { + logger.errorf(e, "Unexpected exception when validating password of DN [%s]", dn); + throw new AuthenticationException("Unexpected exception when validating password of user"); } finally { if (authCtx != null) { try { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java index 2a79a48673..4990817037 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java @@ -3,6 +3,8 @@ package org.keycloak.federation.ldap.mappers; import java.util.Collections; import java.util.List; +import javax.naming.AuthenticationException; + import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; @@ -70,6 +72,10 @@ public abstract class AbstractLDAPFederationMapper { return Collections.emptyList(); } + public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) { + return false; + } + public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { String paramm = mapperModel.getConfig().get(paramName); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java index 8d5ab4ea86..a27b9bf1ba 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java @@ -1,5 +1,7 @@ package org.keycloak.federation.ldap.mappers; +import javax.naming.AuthenticationException; + import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; @@ -59,4 +61,17 @@ public interface LDAPFederationMapper extends UserFederationMapper { * @param query */ void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query); + + /** + * Called when LDAP authentication of specified user fails. If any mapper returns true from this method, AuthenticationException won't be rethrown! + * + * @param mapperModel + * @param ldapProvider + * @param realm + * @param user + * @param ldapUser + * @param ldapException + * @return true if mapper processed the AuthenticationException and did some actions based on that. In that case, AuthenticationException won't be rethrown! + */ + boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java index b77bf3836f..3bc4451ff7 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapperBridge.java @@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.mappers; import java.util.List; +import javax.naming.AuthenticationException; + import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; @@ -55,7 +57,7 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper { @Override public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { // Improve if needed - getDelegate(mapperModel, null, null).beforeLDAPQuery(query); + getDelegate(mapperModel, query.getLdapProvider(), null).beforeLDAPQuery(query); } @@ -64,6 +66,11 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper { return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults); } + @Override + public boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm) { + return getDelegate(mapperModel, ldapProvider, realm).onAuthenticationFailure(ldapUser, user, ldapException); + } + private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) { LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; return factory.createMapper(mapperModel, ldapProvider, realm); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java new file mode 100644 index 0000000000..cd233a1dc1 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java @@ -0,0 +1,249 @@ +package org.keycloak.federation.ldap.mappers.msad; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.naming.AuthenticationException; + +import org.jboss.logging.Logger; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.UserModelDelegate; + +/** + * Mapper specific to MSAD. It's able to read the userAccountControl and pwdLastSet attributes and set actions in Keycloak based on that. + * It's also able to handle exception code from LDAP user authentication (See http://www-01.ibm.com/support/docview.wss?uid=swg21290631 ) + * + * @author Marek Posolda + */ +public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper { + + private static final Logger logger = Logger.getLogger(MSADUserAccountControlMapper.class); + + private static final Pattern AUTH_EXCEPTION_REGEX = Pattern.compile(".*AcceptSecurityContext error, data ([0-9a-f]*), v.*"); + + public MSADUserAccountControlMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { + super(mapperModel, ldapProvider, realm); + } + + @Override + public void beforeLDAPQuery(LDAPQuery query) { + query.addReturningLdapAttribute(LDAPConstants.PWD_LAST_SET); + query.addReturningLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL); + + // This needs to be read-only and can be set to writable just on demand + query.addReturningReadOnlyLdapAttribute(LDAPConstants.PWD_LAST_SET); + + if (ldapProvider.getEditMode() != UserFederationProvider.EditMode.WRITABLE) { + query.addReturningReadOnlyLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL); + } + } + + @Override + public UserModel proxy(LDAPObject ldapUser, UserModel delegate) { + return new MSADUserModelDelegate(delegate, ldapUser); + } + + @Override + public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) { + + } + + @Override + public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) { + + } + + @Override + public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) { + String exceptionMessage = ldapException.getMessage(); + Matcher m = AUTH_EXCEPTION_REGEX.matcher(exceptionMessage); + if (m.matches()) { + String errorCode = m.group(1); + return processAuthErrorCode(errorCode, user); + } else { + return false; + } + } + + protected boolean processAuthErrorCode(String errorCode, UserModel user) { + logger.debugf("MSAD Error code is '%s' after failed LDAP login of user", errorCode, user.getUsername()); + + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) { + if (errorCode.equals("532") || errorCode.equals("773")) { + // User needs to change his MSAD password. Allow him to login, but add UPDATE_PASSWORD required action + user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + return true; + } else if (errorCode.equals("533")) { + // User is disabled in MSAD. Set him to disabled in KC as well + user.setEnabled(false); + return true; + } + } + + return false; + } + + + public class MSADUserModelDelegate extends UserModelDelegate { + + private final LDAPObject ldapUser; + + public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) { + super(delegate); + this.ldapUser = ldapUser; + } + + @Override + public boolean isEnabled() { + boolean kcEnabled = super.isEnabled(); + + if (getPwdLastSet() > 0) { + // Merge KC and MSAD + return kcEnabled && !getUserAccountControl().has(UserAccountControl.ACCOUNTDISABLE); + } else { + // If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway + return kcEnabled; + } + } + + @Override + public void setEnabled(boolean enabled) { + // Always update DB + super.setEnabled(enabled); + + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && getPwdLastSet() > 0) { + logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString()); + + UserAccountControl control = getUserAccountControl(); + if (enabled) { + control.remove(UserAccountControl.ACCOUNTDISABLE); + } else { + control.add(UserAccountControl.ACCOUNTDISABLE); + } + + updateUserAccountControl(control); + } + } + + @Override + public void updateCredential(UserCredentialModel cred) { + // Update LDAP password first + super.updateCredential(cred); + + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && cred.getType().equals(UserCredentialModel.PASSWORD)) { + logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString()); + + // Normally it's read-only + ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET); + + ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1"); + + UserAccountControl control = getUserAccountControl(); + control.remove(UserAccountControl.PASSWD_NOTREQD); + control.remove(UserAccountControl.PASSWORD_EXPIRED); + + if (super.isEnabled()) { + control.remove(UserAccountControl.ACCOUNTDISABLE); + } + + updateUserAccountControl(control); + } + } + + @Override + public void addRequiredAction(RequiredAction action) { + String actionName = action.name(); + addRequiredAction(actionName); + } + + @Override + public void addRequiredAction(String action) { + // Always update DB + super.addRequiredAction(action); + + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) { + logger.debugf("Going to propagate required action UPDATE_PASSWORD to MSAD for ldap user '%s' ", ldapUser.getDn().toString()); + + // Normally it's read-only + ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET); + + ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "0"); + ldapProvider.getLdapIdentityStore().update(ldapUser); + } + } + + @Override + public void removeRequiredAction(RequiredAction action) { + String actionName = action.name(); + removeRequiredAction(actionName); + } + + @Override + public void removeRequiredAction(String action) { + // Always update DB + super.removeRequiredAction(action); + + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) { + + // Don't set pwdLastSet in MSAD when it is new user + UserAccountControl accountControl = getUserAccountControl(); + if (accountControl.getValue() != 0 && !accountControl.has(UserAccountControl.PASSWD_NOTREQD)) { + logger.debugf("Going to remove required action UPDATE_PASSWORD from MSAD for ldap user '%s' ", ldapUser.getDn().toString()); + + // Normally it's read-only + ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET); + + ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1"); + ldapProvider.getLdapIdentityStore().update(ldapUser); + } + } + } + + @Override + public Set getRequiredActions() { + Set requiredActions = super.getRequiredActions(); + + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) { + if (getPwdLastSet() == 0 || getUserAccountControl().has(UserAccountControl.PASSWORD_EXPIRED)) { + requiredActions = new HashSet<>(requiredActions); + requiredActions.add(RequiredAction.UPDATE_PASSWORD.toString()); + return requiredActions; + } + } + + return requiredActions; + } + + protected long getPwdLastSet() { + String pwdLastSet = ldapUser.getAttributeAsString(LDAPConstants.PWD_LAST_SET); + return pwdLastSet == null ? 0 : Long.parseLong(pwdLastSet); + } + + protected UserAccountControl getUserAccountControl() { + String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL); + long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl); + return new UserAccountControl(longValue); + } + + // Update user in LDAP + protected void updateUserAccountControl(UserAccountControl accountControl) { + String userAccountControlValue = String.valueOf(accountControl.getValue()); + logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue); + + ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue); + ldapProvider.getLdapIdentityStore().update(ldapUser); + } + } + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java new file mode 100644 index 0000000000..cacce00879 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java @@ -0,0 +1,68 @@ +package org.keycloak.federation.ldap.mappers.msad; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory; +import org.keycloak.mappers.MapperConfigValidationException; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Marek Posolda + */ +public class MSADUserAccountControlMapperFactory extends AbstractLDAPFederationMapperFactory { + + public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER; + protected static final List configProperties = new ArrayList(); + + static { + } + + @Override + public String getHelpText() { + return "Mapper specific to MSAD. It's able to integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). It's using userAccountControl and pwdLastSet MSAD attributes for that. " + + "For example if pwdLastSet is 0, the Keycloak user is required to update password, if userAccountControl is 514 (disabled account) the Keycloak user is disabled as well etc. Mapper is also able to handle exception code from LDAP user authentication."; + } + + @Override + public String getDisplayCategory() { + return ATTRIBUTE_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "MSAD User Account Control"; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public Map getDefaultConfig(UserFederationProviderModel providerModel) { + return Collections.emptyMap(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + } + + @Override + protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) { + return new MSADUserAccountControlMapper(mapperModel, federationProvider, realm); + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java new file mode 100644 index 0000000000..c7f831715c --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/UserAccountControl.java @@ -0,0 +1,58 @@ +package org.keycloak.federation.ldap.mappers.msad; + +/** + * See https://support.microsoft.com/en-us/kb/305144 + * + * @author Marek Posolda + */ +public class UserAccountControl { + + public static final long SCRIPT = 0x0001l; + public static final long ACCOUNTDISABLE = 0x0002l; + public static final long HOMEDIR_REQUIRED = 0x0008l; + public static final long LOCKOUT = 0x0010l; + public static final long PASSWD_NOTREQD = 0x0020l; + public static final long PASSWD_CANT_CHANGE = 0x0040l; + public static final long ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080l; + public static final long TEMP_DUPLICATE_ACCOUNT = 0x0100l; + public static final long NORMAL_ACCOUNT = 0x0200l; + public static final long INTERDOMAIN_TRUST_ACCOUNT = 0x0800l; + public static final long WORKSTATION_TRUST_ACCOUNT = 0x1000l; + public static final long SERVER_TRUST_ACCOUNT = 0x2000l; + public static final long DONT_EXPIRE_PASSWORD = 0x10000l; + public static final long MNS_LOGON_ACCOUNT = 0x20000l; + public static final long SMARTCARD_REQUIRED = 0x40000l; + public static final long TRUSTED_FOR_DELEGATION = 0x80000l; + public static final long NOT_DELEGATED = 0x100000l; + public static final long USE_DES_KEY_ONLY = 0x200000l; + public static final long DONT_REQ_PREAUTH = 0x400000l; + public static final long PASSWORD_EXPIRED = 0x800000l; + public static final long TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000l; + public static final long PARTIAL_SECRETS_ACCOUNT = 0x04000000l; + + private long value; + + public UserAccountControl(long value) { + this.value = value; + } + + public boolean has(long feature) { + return (this.value & feature) > 0; + } + + public void add(long feature) { + if (!has(feature)) { + this.value += feature; + } + } + + public void remove(long feature) { + if (has(feature)) { + this.value -= feature; + } + } + + public long getValue() { + return value; + } +} diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory index ac130a551e..0575a65042 100644 --- a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory +++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory @@ -2,4 +2,5 @@ org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory -org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory \ No newline at end of file +org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory +org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html index 6263f3d9f6..1ddf888015 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html @@ -183,14 +183,6 @@ Does the LDAP server support pagination. -
    - -
    - -
    - Useful just for Active Directory. If enabled, then Keycloak will always set - Active Directory userAccountControl attribute to 512 after password update. This would mean that particular user will be enabled in Active Directory -
    diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java index 596f2783da..c541b3b652 100755 --- a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java +++ b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java @@ -1,7 +1,5 @@ package org.keycloak.migration; -import org.keycloak.provider.Provider; -import org.keycloak.provider.ProviderFactory; /** * @author Bill Burke @@ -11,7 +9,7 @@ public interface MigrationModel { /** * Must have the form of major.minor.micro as the version is parsed and numbers are compared */ - public static final String LATEST_VERSION = "1.7.0"; + String LATEST_VERSION = "1.8.0"; String getStoredVersion(); void setStoredVersion(String version); diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java index 06df07311c..20918de12f 100755 --- a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -6,6 +6,7 @@ import org.keycloak.migration.migrators.MigrateTo1_4_0; import org.keycloak.migration.migrators.MigrateTo1_5_0; import org.keycloak.migration.migrators.MigrateTo1_6_0; import org.keycloak.migration.migrators.MigrateTo1_7_0; +import org.keycloak.migration.migrators.MigrateTo1_8_0; import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1; import org.keycloak.models.KeycloakSession; @@ -61,6 +62,12 @@ public class MigrationModelManager { } new MigrateTo1_7_0().migrate(session); } + if (stored == null || stored.lessThan(MigrateTo1_8_0.VERSION)) { + if (stored != null) { + logger.debug("Migrating older model to 1.8.0 updates"); + } + new MigrateTo1_8_0().migrate(session); + } model.setStoredVersion(MigrationModel.LATEST_VERSION); } diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_8_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_8_0.java new file mode 100644 index 0000000000..b32f953364 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_8_0.java @@ -0,0 +1,48 @@ +package org.keycloak.migration.migrators; + +import java.util.List; +import java.util.Map; +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public class MigrateTo1_8_0 { + + public static final ModelVersion VERSION = new ModelVersion("1.8.0"); + + public void migrate(KeycloakSession session) { + List realms = session.realms().getRealms(); + for (RealmModel realm : realms) { + + List federationProviders = realm.getUserFederationProviders(); + for (UserFederationProviderModel fedProvider : federationProviders) { + + if (fedProvider.getProviderName().equals(LDAPConstants.LDAP_PROVIDER)) { + Map config = fedProvider.getConfig(); + + if (isActiveDirectory(config)) { + + // Create mapper for MSAD account controls + if (realm.getUserFederationMapperByName(fedProvider.getId(), "MSAD account controls") == null) { + UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel("MSAD account controls", fedProvider.getId(), LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER); + realm.addUserFederationMapper(mapperModel); + } + } + } + } + + } + } + + private boolean isActiveDirectory(Map ldapConfig) { + String vendor = ldapConfig.get(LDAPConstants.VENDOR); + return vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY); + } +} diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java index 2d403f373e..020bfbd02a 100644 --- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java +++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java @@ -6,6 +6,7 @@ package org.keycloak.models; public class LDAPConstants { public static final String LDAP_PROVIDER = "ldap"; + public static final String MSAD_USER_ACCOUNT_CONTROL_MAPPER = "msad-user-account-control-mapper"; public static final String VENDOR = "vendor"; public static final String VENDOR_RHDS = "rhds"; @@ -43,9 +44,6 @@ public class LDAPConstants { // Config option to specify if registrations will be synced or not public static final String SYNC_REGISTRATIONS = "syncRegistrations"; - // Applicable just for active directory - public static final String USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "userAccountControlsAfterPasswordUpdate"; - // Custom user search filter public static final String CUSTOM_USER_SEARCH_FILTER = "customUserSearchFilter"; @@ -53,9 +51,6 @@ public class LDAPConstants { public static final String LDAP_ID = "LDAP_ID"; public static final String LDAP_ENTRY_DN = "LDAP_ENTRY_DN"; - // String used in config to divide more possible values (for example more userDns), which are saved in DB as single string - public static final String CONFIG_DIVIDER = ":::"; - // Those are forked from Picketlink public static final String GIVENNAME = "givenName"; public static final String CN = "cn"; @@ -73,6 +68,8 @@ public class LDAPConstants { public static final String GROUP_OF_NAMES = "groupOfNames"; public static final String GROUP_OF_ENTRIES = "groupOfEntries"; public static final String GROUP_OF_UNIQUE_NAMES = "groupOfUniqueNames"; + public static final String USER_ACCOUNT_CONTROL = "userAccountControl"; + public static final String PWD_LAST_SET = "pwdLastSet"; public static final String COMMA = ","; public static final String EQUAL = "="; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPTestConfiguration.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPTestConfiguration.java index 06c8f5ca47..3920208251 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPTestConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPTestConfiguration.java @@ -39,7 +39,6 @@ public class LDAPTestConfiguration { PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute"); PROP_MAPPINGS.put(LDAPConstants.RDN_LDAP_ATTRIBUTE, "idm.test.ldap.rdn.ldap.attribute"); PROP_MAPPINGS.put(LDAPConstants.USER_OBJECT_CLASSES, "idm.test.ldap.user.object.classes"); - PROP_MAPPINGS.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "idm.test.ldap.user.account.controls.after.password.update"); PROP_MAPPINGS.put(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode"); PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication"); @@ -62,7 +61,6 @@ public class LDAPTestConfiguration { DEFAULT_VALUES.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC)); DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null); DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null); - DEFAULT_VALUES.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "false"); DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString()); DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java index 46b1be4d61..7b1f4c2358 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java @@ -40,7 +40,6 @@ public class LDAPTestConfiguration { PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute"); PROP_MAPPINGS.put(LDAPConstants.RDN_LDAP_ATTRIBUTE, "idm.test.ldap.rdn.ldap.attribute"); PROP_MAPPINGS.put(LDAPConstants.USER_OBJECT_CLASSES, "idm.test.ldap.user.object.classes"); - PROP_MAPPINGS.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "idm.test.ldap.user.account.controls.after.password.update"); PROP_MAPPINGS.put(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode"); PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication"); @@ -63,7 +62,6 @@ public class LDAPTestConfiguration { DEFAULT_VALUES.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC)); DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null); DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null); - DEFAULT_VALUES.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "false"); DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString()); DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java index 1d41595f6f..b07e23dfcc 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java @@ -749,7 +749,7 @@ public class FederationProvidersIntegrationTest { } @Test - public void testUnsynced() { + public void testUnsynced() throws Exception { KeycloakSession session = keycloakRule.startSession(); try { RealmModel appRealm = session.realms().getRealmByName("test"); From 133e4c59e56b045c589e70e514d1b6976929f6b9 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 5 Jan 2016 15:25:22 +0100 Subject: [PATCH 48/65] KEYCLOAK-2258 Removing leftovers of file model from docs and testsuite --- .../en/en-US/modules/server-installation.xml | 80 ------------------- testsuite/integration/pom.xml | 26 ------ 2 files changed, 106 deletions(-) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml index 4dc4b323b8..38ca3f8f42 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml @@ -370,86 +370,6 @@ bin/add-user.[sh|bat] -r master -u -p - -
    - JSON File based model - - Keycloak provides a JSON file based model implementation, which means that your identity data will be saved - in a flat JSON text file instead of traditional RDBMS. The performance of this implementaion is likely to - be slower because it reads and writes the entire file with each call to the Keycloak REST API. But it is - very useful in development to see exactly what is being saved. It is not recommended for - production. - - - Note that this only applies to realm and user data. There is currently no file implementation for - event persistence. So you will need to use JPA or Mongo for that. - - - To configure Keycloak to use file persistence open standalone/configuration/keycloak-server.json - in your favourite editor. Change the realm and user providers and disable caching. Change: - - - - to: - - - -You can also change the location of the data file by adding a connectionsFile snippet: - -All configuration options are optional. Default value for directory is ${jboss.server.data.dir}. Default file name - is keycloak-model.json. - -
    Outgoing Server HTTP Requests diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 44d1e5045d..db31b7f61f 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -369,32 +369,6 @@ - - file - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/broker/*** - **/CacheTest.java - - - file - file - none - none - ${project.build.directory} - - - - - - - mongo From 39d5a0721869b2f55a007369c206cd4a8856ef84 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 5 Jan 2016 10:59:13 -0500 Subject: [PATCH 49/65] KEYCLOAK-2221 --- .../protocol/oidc/mappers/RoleNameMapper.java | 2 +- .../testsuite/oauth/AccessTokenTest.java | 67 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java index 5f0178cc6f..3634095376 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java @@ -90,7 +90,7 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc access.getRoles().remove(roleName); } else { AccessToken.Access access = token.getRealmAccess(); - if (access == null) return token; + if (access == null || !access.getRoles().contains(roleName)) return token; access.getRoles().remove(roleName); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 4e7629a2ce..0b45d64ba0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -48,6 +48,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; @@ -107,6 +109,14 @@ public class AccessTokenTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true); + { // for KEYCLOAK-2221 + UserModel user = manager.getSession().users().addUser(appRealm, KeycloakModelUtils.generateId(), "no-permissions", false, false); + user.updateCredential(UserCredentialModel.password("password")); + user.setEnabled(true); + RoleModel role = appRealm.getRole("user"); + user.grantRole(role); + } + keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID); } @@ -670,6 +680,53 @@ public class AccessTokenTest { } + @Test + public void testKeycloak2221() throws Exception { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + { + KeycloakSession session = keycloakRule.startSession(); + RealmModel realm = session.realms().getRealmByName("test"); + ClientModel app = realm.getClientByClientId("test-app"); + app.addProtocolMapper(RoleNameMapper.create("rename-role", "user", "realm-user")); + app.addProtocolMapper(RoleNameMapper.create("rename-role2", "admin", "the-admin")); + session.getTransaction().commit(); + session.close(); + } + + { + Response response = executeGrantRequest(grantTarget, "no-permissions", "password"); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + AccessToken accessToken = getAccessToken(tokenResponse); + Assert.assertEquals(accessToken.getRealmAccess().getRoles().size(), 1); + Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user")); + + + response.close(); + } + + // undo mappers + { + KeycloakSession session = keycloakRule.startSession(); + RealmModel realm = session.realms().getRealmByName("test"); + ClientModel app = realm.getClientByClientId("test-app"); + for (ProtocolMapperModel model : app.getProtocolMappers()) { + if (model.getName().startsWith("rename-role")) { + app.removeProtocolMapper(model); + } + } + session.getTransaction().commit(); + session.close(); + } + + events.clear(); + + + } + @Test public void testTokenMapping() throws Exception { @@ -1094,11 +1151,17 @@ public class AccessTokenTest { } protected Response executeGrantAccessTokenRequest(WebTarget grantTarget) { + String username = "test-user@localhost"; + String password = "password"; + return executeGrantRequest(grantTarget, username, password); + } + + protected Response executeGrantRequest(WebTarget grantTarget, String username, String password) { String header = BasicAuthHelper.createHeader("test-app", "password"); Form form = new Form(); form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD) - .param("username", "test-user@localhost") - .param("password", "password"); + .param("username", username) + .param("password", password); return grantTarget.request() .header(HttpHeaders.AUTHORIZATION, header) .post(Entity.form(form)); From 04401af470e65d1047597d1812728b4783db34fd Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 5 Jan 2016 16:45:14 +0100 Subject: [PATCH 50/65] Fix testsuite to pass with MSAD --- testsuite/integration/pom.xml | 22 +++++++++++++++ .../federation/ldap/FederationTestUtils.java | 10 +++++++ .../FederationProvidersIntegrationTest.java | 28 +++++++++---------- .../ldap/base/LDAPGroupMapperTest.java | 6 ++-- .../ldap/base/LDAPMultipleAttributesTest.java | 8 +++--- .../ldap/base/LDAPRoleMappingsTest.java | 6 ++-- .../ldap/base/SyncProvidersTest.java | 2 +- 7 files changed, 57 insertions(+), 25 deletions(-) diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index db31b7f61f..15ca2d38a0 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -511,5 +511,27 @@ + + + + msad + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org/keycloak/testsuite/federation/ldap/base/** + + + **/LDAPMultipleAttributesTest.java + + + + + + + diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java index f189e2012c..0f357d3e39 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java @@ -93,6 +93,16 @@ public class FederationTestUtils { return LDAPUtils.addUserToLDAP(ldapProvider, realm, helperUser); } + public static void updateLDAPPassword(LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String password) { + ldapProvider.getLdapIdentityStore().updatePassword(ldapUser, password); + + // Enable MSAD user through userAccountControls + if (ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) { + ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, "512"); + ldapProvider.getLdapIdentityStore().update(ldapUser); + } + } + public static LDAPFederationProvider getLdapProvider(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) { LDAPFederationProviderFactory ldapProviderFactory = (LDAPFederationProviderFactory) keycloakSession.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, ldapFedModel.getProviderName()); return ldapProviderFactory.getInstance(keycloakSession, ldapFedModel); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java index b07e23dfcc..0192bbca7a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java @@ -79,7 +79,7 @@ public class FederationProvidersIntegrationTest { FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); LDAPObject existing = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678"); @@ -132,9 +132,9 @@ public class FederationProvidersIntegrationTest { RealmModel appRealm = manager.getRealm("test"); LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); LDAPObject jbrown2 = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "JBrown2", "John", "Brown2", "jbrown2@email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(jbrown2, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, jbrown2, "Password1"); LDAPObject jbrown3 = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown3", "John", "Brown3", "JBrown3@email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(jbrown3, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, jbrown3, "Password1"); } finally { keycloakRule.stopSession(session, true); } @@ -165,10 +165,10 @@ public class FederationProvidersIntegrationTest { RealmManager manager = new RealmManager(session); RealmModel appRealm = manager.getRealm("test"); LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - LDAPObject jbrown2 = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "JBrown4", "John", "Brown4", "jbrown4@email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(jbrown2, "Password1"); - LDAPObject jbrown3 = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown5", "John", "Brown5", "JBrown5@Email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(jbrown3, "Password1"); + LDAPObject jbrown4 = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "JBrown4", "John", "Brown4", "jbrown4@email.org", null, "1234"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, jbrown4, "Password1"); + LDAPObject jbrown5 = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown5", "John", "Brown5", "JBrown5@Email.org", null, "1234"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, jbrown5, "Password1"); } finally { keycloakRule.stopSession(session, true); } @@ -371,7 +371,7 @@ public class FederationProvidersIntegrationTest { } @Test - public void testDotInUsername() { + public void testCommaInUsername() { KeycloakSession session = keycloakRule.startSession(); boolean skip = false; @@ -379,23 +379,23 @@ public class FederationProvidersIntegrationTest { RealmModel appRealm = new RealmManager(session).getRealmByName("test"); LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - // Workaround as dot is not allowed in sAMAccountName on active directory. So we will skip the test for this configuration + // Workaround as comma is not allowed in sAMAccountName on active directory. So we will skip the test for this configuration LDAPConfig config = ldapFedProvider.getLdapIdentityStore().getConfig(); if (config.isActiveDirectory() && config.getUsernameLdapAttribute().equals(LDAPConstants.SAM_ACCOUNT_NAME)) { skip = true; } if (!skip) { - LDAPObject johnDot = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "john,dot", "John", "Dot", "johndot@email.org", null, "12387"); - ldapFedProvider.getLdapIdentityStore().updatePassword(johnDot, "Password1"); + LDAPObject johnComma = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "john,comma", "John", "Comma", "johncomma@email.org", null, "12387"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, johnComma, "Password1"); } } finally { keycloakRule.stopSession(session, false); } if (!skip) { - // Try to import the user with dot in username into Keycloak - loginSuccessAndLogout("john,dot", "Password1"); + // Try to import the user with comma in username into Keycloak + loginSuccessAndLogout("john,comma", "Password1"); } } @@ -583,7 +583,7 @@ public class FederationProvidersIntegrationTest { FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary1", "Kelly1", "mary1@email.org", null, "123"); FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "mary-duplicatemail", "Mary2", "Kelly2", "mary@test.com", null, "123"); LDAPObject marynoemail = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marynoemail", "Mary1", "Kelly1", null, null, "123"); - ldapFedProvider.getLdapIdentityStore().updatePassword(marynoemail, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, marynoemail, "Password1"); } }); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java index 8778735291..de7508f1d1 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperTest.java @@ -84,13 +84,13 @@ public class LDAPGroupMapperTest { // Add some LDAP users for testing LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678"); - ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, mary, "Password1"); LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910"); - ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, rob, "Password1"); } }); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java index 6bca1ee066..67e8447b3a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPMultipleAttributesTest.java @@ -70,13 +70,13 @@ public class LDAPMultipleAttributesTest { FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); LDAPObject james = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", null, "88441"); - ldapFedProvider.getLdapIdentityStore().updatePassword(james, "password"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1"); // User for testing duplicating surname and postalCode LDAPObject bruce = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "bwilson", "Bruce", "Wilson", "bwilson@keycloak.org", "Elm 5", "88441", "77332"); bruce.setAttribute("sn", new LinkedHashSet<>(Arrays.asList("Wilson", "Schneider"))); ldapFedProvider.getLdapIdentityStore().update(bruce); - ldapFedProvider.getLdapIdentityStore().updatePassword(bruce, "password"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, bruce, "Password1"); // Create ldap-portal client ClientModel ldapClient = KeycloakModelUtils.createClient(appRealm, "ldap-portal"); @@ -174,7 +174,7 @@ public class LDAPMultipleAttributesTest { // Login as bwilson driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal"); Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); - loginPage.login("bwilson", "password"); + loginPage.login("bwilson", "Password1"); Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal")); String pageSource = driver.getPageSource(); System.out.println(pageSource); @@ -190,7 +190,7 @@ public class LDAPMultipleAttributesTest { // Login as jbrown driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal"); Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); - loginPage.login("jbrown", "password"); + loginPage.login("jbrown", "Password1"); Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal")); pageSource = driver.getPageSource(); System.out.println(pageSource); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java index 50cc6d2590..8aa4e49dc7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPRoleMappingsTest.java @@ -75,13 +75,13 @@ public class LDAPRoleMappingsTest { // Add some users for testing LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); - ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678"); - ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, mary, "Password1"); LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910"); - ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, rob, "Password1"); // Add some roles for testing FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java index 39ef19e5c6..a46475eb7e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/SyncProvidersTest.java @@ -65,7 +65,7 @@ public class SyncProvidersTest { for (int i=1 ; i<=5 ; i++) { LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", null, "12" + i); - ldapFedProvider.getLdapIdentityStore().updatePassword(ldapUser, "Password1"); + FederationTestUtils.updateLDAPPassword(ldapFedProvider, ldapUser, "Password1"); } // Add dummy provider From 658f204d9216acb55fa57bfffefda352ce1d9091 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 5 Jan 2016 18:50:54 +0100 Subject: [PATCH 51/65] Documentation for new LDAP mappers --- .../en/en-US/modules/user-federation.xml | 48 +++++++++++++++++++ .../GroupLDAPFederationMapperFactory.java | 2 +- .../role/RoleLDAPFederationMapperFactory.java | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/user-federation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/user-federation.xml index 272c2f69b8..ca82535d78 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/user-federation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/user-federation.xml @@ -202,6 +202,54 @@ + + Hardcoded Role Mapper + + + This mapper will grant specified Keycloak role to each Keycloak user linked with LDAP. + + + + + Group Mapper + + + This allows to configure group mappings from LDAP into Keycloak group mappings. Group mapper can be used to map LDAP groups from particular branch of LDAP tree + into groups in Keycloak. And it will also propagate user-group mappings from LDAP into user-group mappings in Keycloak. + + + You can choose to preserve group inheritance from LDAP as well, but this may fail as Keycloak inheritance is more restrictive than LDAP + (For example in Keycloak each group can have just one parent and there is no recursion allowed. In LDAP the recursion is possible and every group can be member of more + other groups too). + + + As of now, the mapper doesn't provide mapping of LDAP roles-groups to Keycloak roles-groups + (For example when LDAP group cn=role1,ou=roles,dc=example,dc=com is member of LDAP group + cn=group1,ou=groups,dc=example,dc=com , we don't support the mapping of Keycloak role role1 imported from LDAP to corresponding Keycloak group group1 imported from LDAP). + + + + + MSAD User Account Mapper + + + Mapper specific to Microsoft Active Directory (MSAD). It's able to tightly integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). + It's using userAccountControl and pwdLastSet LDAP attributes for that (both are specific to MSAD and are not LDAP standard). + For example if pwdLastSet is 0, the Keycloak user is required to update password (there will be UPDATE_PASSWORD required action added to him in Keycloak). Or if userAccountControl + is 514 (disabled account) the Keycloak user is disabled as well etc. + + + For writable LDAP, the mapping is bi-directional and the state from Keycloak is propagated to LDAP (For example enable user + in Keycloak admin console will update the value of userAccountControl in MSAD and effectively enable him in MSAD as well). + + + For writable LDAPs, mapper also provides mapping of error codes during MSAD user authentication to the + appropriate action in Keycloak. For example if MSAD user authentication fails due to the fact, that MSAD password is expired, + the mapper will allow user to authenticate into Keycloak, but it will add UPDATE_PASSWORD required action to the user, so user + must update his password. + + + By default, there is set of User Attribute mappers to map basic UserModel attributes username, first name, lastname and email to corresponding LDAP attributes. You are free to extend this and provide diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java index fa43ac9721..c2134f114d 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java @@ -78,7 +78,7 @@ public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapp ProviderConfigProperty ldapFilter = createConfigProperty(GroupMapperConfig.GROUPS_LDAP_FILTER, "LDAP Filter", - "LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'", + "LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(ldapFilter); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java index fe0b1e194c..58023aa9fc 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java @@ -72,7 +72,7 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty ldapFilter = createConfigProperty(RoleMapperConfig.ROLES_LDAP_FILTER, "LDAP Filter", - "LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'", + "LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(ldapFilter); From 7ec02761efc264b475a8c76f35d5684c9ef51c93 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 5 Jan 2016 20:40:40 +0100 Subject: [PATCH 52/65] LDAP testing: added activation --- testsuite/integration/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 15ca2d38a0..017e8d1fed 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -514,6 +514,12 @@ + + + ldap.vendor + msad + + msad From 7ec0dad88f5e06674442d9b43bfb98efaea94e31 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Jan 2016 18:09:43 -0200 Subject: [PATCH 53/65] Make use of display name by default for TOTP - use realm name just in case of display name is not available --- .../java/org/keycloak/account/freemarker/model/TotpBean.java | 2 -- .../java/org/keycloak/login/freemarker/model/TotpBean.java | 2 -- model/api/src/main/java/org/keycloak/models/OTPPolicy.java | 5 ++++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java index 419f3213e5..11363fa05b 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java @@ -41,11 +41,9 @@ public class TotpBean { private final String totpSecretEncoded; private final boolean enabled; private final String contextUrl; - private final String realmName; private final String keyUri; public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri) { - this.realmName = realm.getName(); this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); this.contextUrl = baseUri.getPath(); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java index 6c9def495f..eae56dc39e 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java @@ -39,11 +39,9 @@ public class TotpBean { private final String totpSecretEncoded; private final boolean enabled; private final String contextUrl; - private final String realmName; private final String keyUri; public TotpBean(RealmModel realm, UserModel user, URI baseUri) { - this.realmName = realm.getName(); this.enabled = user.isOtpEnabled(); this.contextUrl = baseUri.getPath(); diff --git a/model/api/src/main/java/org/keycloak/models/OTPPolicy.java b/model/api/src/main/java/org/keycloak/models/OTPPolicy.java index 157842e94e..8d76bb9b83 100755 --- a/model/api/src/main/java/org/keycloak/models/OTPPolicy.java +++ b/model/api/src/main/java/org/keycloak/models/OTPPolicy.java @@ -97,8 +97,11 @@ public class OTPPolicy implements Serializable { public String getKeyURI(RealmModel realm, UserModel user, String secret) { + String displayName = realm.getDisplayName(); String uri = null; - uri = "otpauth://" + type + "/" + realm.getName() + ":" + user.getUsername() + "?secret=" + + + if (displayName == null || displayName.isEmpty()) { displayName = realm.getName(); } + uri = "otpauth://" + type + "/" + displayName + ":" + user.getUsername() + "?secret=" + Base32.encode(secret.getBytes()) + "&digits=" + digits + "&algorithm=" + algToKeyUriAlg.get(algorithm); try { uri += "&issuer=" + URLEncoder.encode(realm.getName(), "UTF-8"); From 2a8e868bc18ad6e9bd8c909a6e976bd668c6796e Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Tue, 5 Jan 2016 22:52:16 +0100 Subject: [PATCH 54/65] Fix missing offlineSession local-cache config for demo-dist --- distribution/demo-dist/src/main/xslt/standalone.xsl | 1 + 1 file changed, 1 insertion(+) diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl index 2110908134..7a90804995 100755 --- a/distribution/demo-dist/src/main/xslt/standalone.xsl +++ b/distribution/demo-dist/src/main/xslt/standalone.xsl @@ -70,6 +70,7 @@ + From aca00dd42c25cd547e906d440a752410565865aa Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Tue, 5 Jan 2016 23:06:26 +0100 Subject: [PATCH 55/65] Prevent ProviderFactories being loaded twice --- .../java/org/keycloak/provider/ProviderManager.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java index 6246d83973..3f250da672 100644 --- a/services/src/main/java/org/keycloak/provider/ProviderManager.java +++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java @@ -3,6 +3,7 @@ package org.keycloak.provider; import org.jboss.logging.Logger; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -52,10 +53,17 @@ public class ProviderManager { List factories = cache.get(spi.getName()); if (factories == null) { factories = new LinkedList(); + IdentityHashMap factoryClasses = new IdentityHashMap(); for (ProviderLoader loader : loaders) { List f = loader.load(spi); if (f != null) { - factories.addAll(f); + for (ProviderFactory pf: f) { + // make sure there are no duplicates + if (!factoryClasses.containsKey(pf.getClass())) { + factories.add(pf); + factoryClasses.put(pf.getClass(), pf); + } + } } } } From 4a472b8272218b2496f5e8cceee5fcc219f9220b Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 6 Jan 2016 10:02:56 -0500 Subject: [PATCH 56/65] KEYCLOAK-1990 KEYCLOAK-1991 --- .../models/utils/RepresentationToModel.java | 10 ++++++- .../keycloak/testsuite/admin/RealmTest.java | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) mode change 100644 => 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 3b2cfdc5db..f40f9628c9 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -1,5 +1,6 @@ package org.keycloak.models.utils; +import org.keycloak.Config; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.Constants; import org.keycloak.common.util.Base64; @@ -593,9 +594,16 @@ public class RepresentationToModel { } } + public static void renameRealm(RealmModel realm, String name) { + if (name.equals(realm.getName())) return; + ClientModel masterApp = realm.getMasterAdminClient(); + masterApp.setClientId(KeycloakModelUtils.getMasterRealmAdminApplicationClientId(name)); + realm.setName(name); + } + public static void updateRealm(RealmRepresentation rep, RealmModel realm) { if (rep.getRealm() != null) { - realm.setName(rep.getRealm()); + renameRealm(realm, rep.getRealm()); } if (rep.getDisplayName() != null) realm.setDisplayName(rep.getDisplayName()); if (rep.getDisplayNameHtml() != null) realm.setDisplayNameHtml(rep.getDisplayNameHtml()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java old mode 100644 new mode 100755 index b2fb326c5d..4f0936be4a --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -99,6 +99,33 @@ public class RealmTest extends AbstractClientTest { ServerInfoResource serverInfoResource = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID).serverInfo(); serverInfoResource.getInfo(); + } + + /** + * KEYCLOAK-1990 1991 + * @throws Exception + */ + @Test + public void renameRealmTest() throws Exception { + Keycloak keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID); + RealmRepresentation realm1 = new RealmRepresentation(); + realm1.setRealm("test-immutable"); + keycloak.realms().create(realm1); + realm1 = keycloak.realms().realm("test-immutable").toRepresentation(); + realm1.setRealm("test-immutable-old"); + keycloak.realms().realm("test-immutable").update(realm1); + realm1 = keycloak.realms().realm("test-immutable-old").toRepresentation(); + + RealmRepresentation realm2 = new RealmRepresentation(); + realm2.setRealm("test-immutable"); + keycloak.realms().create(realm2); + realm2 = keycloak.realms().realm("test-immutable").toRepresentation(); + + keycloak.realms().realm("test-immutable-old").remove(); + + + + } @Test From a5c159eeffaec051a15b94ac5129c42d511fce29 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 5 Jan 2016 19:15:26 +0100 Subject: [PATCH 57/65] KEYCLOAK-2247 Upgrade to WildFly 10.0.0.CR5 --- .../feature-pack-build.xml | 37 +- .../configuration/domain/subsystems.xml | 59 --- .../configuration/domain/template.xml | 34 +- .../configuration/host/host-master.xml | 85 ++++ .../configuration/host/host-slave.xml | 101 +++++ .../resources/configuration/host/host.xml | 110 +++++ .../configuration/host/subsystems.xml | 29 ++ .../standalone/subsystems-ha.xml | 52 +-- .../configuration/standalone/template.xml | 17 +- .../keycloak/models/UserSessionProvider.java | 8 +- .../CompatInfinispanUserSessionProvider.java | 387 ++++++++++++++++++ .../models/sessions/infinispan/Consumers.java | 66 +++ .../InfinispanUserSessionProvider.java | 364 +++++++--------- .../InfinispanUserSessionProviderFactory.java | 7 +- .../infinispan/UserSessionTimestamp.java | 24 ++ .../compat/MemUserSessionProvider.java | 17 +- .../InfinispanUserSessionInitializer.java | 14 +- .../stream/ClientInitialAccessPredicate.java | 59 +++ .../stream/ClientSessionPredicate.java | 97 +++++ .../infinispan/stream/Comparators.java | 24 ++ .../sessions/infinispan/stream/Mappers.java | 77 ++++ .../infinispan/stream/SessionPredicate.java | 29 ++ .../stream/UserLoginFailurePredicate.java | 31 ++ .../stream/UserSessionPredicate.java | 91 ++++ pom.xml | 8 +- .../resources/admin/ClientResource.java | 8 +- .../resources/admin/RealmAdminResource.java | 4 +- .../scheduled/ClearExpiredUserSessions.java | 2 +- .../testsuite/adapter/AdapterTest.java | 2 + .../adapter/AdapterTestStrategy.java | 2 +- .../testsuite/forms/BruteForceTest.java | 3 - .../keycloak/testsuite/forms/LoginTest.java | 2 +- .../model/UserSessionProviderOfflineTest.java | 4 +- .../model/UserSessionProviderTest.java | 32 +- .../testsuite/oauth/OfflineTokenTest.java | 2 +- 35 files changed, 1502 insertions(+), 386 deletions(-) create mode 100644 distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-master.xml create mode 100644 distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-slave.xml create mode 100644 distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host.xml create mode 100644 distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/subsystems.xml create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CompatInfinispanUserSessionProvider.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionTimestamp.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientInitialAccessPredicate.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java create mode 100755 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java create mode 100755 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java diff --git a/distribution/feature-packs/server-feature-pack/feature-pack-build.xml b/distribution/feature-packs/server-feature-pack/feature-pack-build.xml index f9208857aa..6ef7e7bb6b 100644 --- a/distribution/feature-packs/server-feature-pack/feature-pack-build.xml +++ b/distribution/feature-packs/server-feature-pack/feature-pack-build.xml @@ -1,4 +1,26 @@ - + + + @@ -8,20 +30,31 @@ + + + + + + + + + + + @@ -33,4 +66,4 @@ - \ No newline at end of file + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/subsystems.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/subsystems.xml index 29c6e63889..bea2e10873 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/subsystems.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/subsystems.xml @@ -53,63 +53,4 @@ undertow.xml keycloak-server.xml - - - logging.xml - bean-validation.xml - keycloak-datasources.xml - ee.xml - ejb3.xml - io.xml - keycloak-infinispan.xml - iiop-openjdk.xml - jaxrs.xml - jca.xml - jdr.xml - jmx.xml - jpa.xml - jsf.xml - jsr77.xml - mail.xml - messaging.xml - naming.xml - remoting.xml - request-controller.xml - security.xml - security-manager.xml - transactions.xml - undertow.xml - keycloak-server.xml - - - - logging.xml - bean-validation.xml - keycloak-datasources.xml - ee.xml - ejb3.xml - io.xml - keycloak-infinispan.xml - iiop-openjdk.xml - jaxrs.xml - jca.xml - jdr.xml - jgroups.xml - jmx.xml - jpa.xml - jsf.xml - jsr77.xml - mail.xml - messaging.xml - mod_cluster.xml - naming.xml - remoting.xml - resource-adapters.xml - request-controller.xml - security.xml - security-manager.xml - transactions.xml - undertow.xml - keycloak-server.xml - diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/template.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/template.xml index 8c4464c591..770d2619bd 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/template.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/domain/template.xml @@ -24,18 +24,12 @@ - - - - - - - - - - - - + + + + + + - - - - - - - - - + - + - + - + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-master.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-master.xml new file mode 100644 index 0000000000..5d959bd093 --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-master.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-slave.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-slave.xml new file mode 100644 index 0000000000..4b79b0aedc --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host-slave.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host.xml new file mode 100644 index 0000000000..501fae8931 --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/host.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/subsystems.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/subsystems.xml new file mode 100644 index 0000000000..3df2a95f79 --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/host/subsystems.xml @@ -0,0 +1,29 @@ + + + + + + + jmx.xml + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/subsystems-ha.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/subsystems-ha.xml index 151ab710de..bf84e794ee 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/subsystems-ha.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/subsystems-ha.xml @@ -1,30 +1,30 @@ - - logging.xml - bean-validation.xml - keycloak-datasources.xml - ee.xml - ejb3.xml - io.xml - keycloak-infinispan.xml - jaxrs.xml - jca.xml - jdr.xml - jgroups.xml - jmx.xml - jpa.xml - jsf.xml - mail.xml - mod_cluster.xml - naming.xml - remoting.xml - request-controller.xml - security-manager.xml - security.xml - transactions.xml - undertow.xml - keycloak-server.xml - + + logging.xml + bean-validation.xml + keycloak-datasources.xml + ee.xml + ejb3.xml + io.xml + keycloak-infinispan.xml + jaxrs.xml + jca.xml + jdr.xml + jgroups.xml + jmx.xml + jpa.xml + jsf.xml + mail.xml + mod_cluster.xml + naming.xml + remoting.xml + request-controller.xml + security-manager.xml + security.xml + transactions.xml + undertow.xml + keycloak-server.xml + \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/template.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/template.xml index 76fbe9e4a4..4df055c0ab 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/template.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/configuration/standalone/template.xml @@ -29,15 +29,15 @@ - - + + - + - - - + + + @@ -69,8 +69,9 @@ - + + @@ -80,4 +81,4 @@ - \ No newline at end of file + diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index 0c1df9cc1f..f2acf49115 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -23,14 +23,12 @@ public interface UserSessionProvider extends Provider { List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId); UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId); - List getUserSessionsByNote(RealmModel realm, String noteName, String noteValue); - - int getActiveUserSessions(RealmModel realm, ClientModel client); + long getActiveUserSessions(RealmModel realm, ClientModel client); void removeUserSession(RealmModel realm, UserSessionModel session); void removeUserSessions(RealmModel realm, UserModel user); // Implementation should propagate removal of expired userSessions to userSessionPersister too - void removeExpiredUserSessions(RealmModel realm); + void removeExpired(RealmModel realm); void removeUserSessions(RealmModel realm); void removeClientSession(RealmModel realm, ClientSessionModel clientSession); @@ -56,7 +54,7 @@ public interface UserSessionProvider extends Provider { // Don't remove userSession even if it's last userSession void removeOfflineClientSession(RealmModel realm, String clientSessionId); - int getOfflineSessionsCount(RealmModel realm, ClientModel client); + long getOfflineSessionsCount(RealmModel realm, ClientModel client); List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max); // Triggered by persister during pre-load diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CompatInfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CompatInfinispanUserSessionProvider.java new file mode 100644 index 0000000000..bcd72e79ef --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CompatInfinispanUserSessionProvider.java @@ -0,0 +1,387 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.distexec.mapreduce.MapReduceTask; +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.models.*; +import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.entities.*; +import org.keycloak.models.sessions.infinispan.mapreduce.*; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmInfoUtil; + +import java.util.*; + +/** + * @author Stian Thorgersen + */ +public class CompatInfinispanUserSessionProvider extends InfinispanUserSessionProvider { + + private static final Logger log = Logger.getLogger(CompatInfinispanUserSessionProvider.class); + + public CompatInfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache offlineSessionCache, + Cache loginFailureCache) { + super(session, sessionCache, offlineSessionCache, loginFailureCache); + } + + @Override + public List getUserSessions(RealmModel realm, UserModel user) { + Map sessions = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + + return wrapUserSessions(realm, sessions.values(), false); + } + + @Override + public List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId) { + Map sessions = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).brokerUserId(brokerUserId)) + .reducedWith(new FirstResultReducer()) + .execute(); + + return wrapUserSessions(realm, sessions.values(), false); + } + + @Override + public UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { + Map sessions = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).brokerSessionId(brokerSessionId)) + .reducedWith(new FirstResultReducer()) + .execute(); + + List userSessionModels = wrapUserSessions(realm, sessions.values(), false); + if (userSessionModels.isEmpty()) return null; + return userSessionModels.get(0); + } + + @Override + public List getUserSessions(RealmModel realm, ClientModel client) { + return getUserSessions(realm, client, -1, -1); + } + + @Override + public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { + return getUserSessions(realm, client, firstResult, maxResults, false); + } + + protected List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults, boolean offline) { + Cache cache = getCache(offline); + + Map map = new MapReduceTask(cache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) + .reducedWith(new LargestResultReducer()) + .execute(); + + List> sessionTimestamps = new LinkedList>(map.entrySet()); + + Collections.sort(sessionTimestamps, new Comparator>() { + @Override + public int compare(Map.Entry e1, Map.Entry e2) { + return e1.getValue().compareTo(e2.getValue()); + } + }); + + if (firstResult != -1 || maxResults == -1) { + if (firstResult == -1) { + firstResult = 0; + } + + if (maxResults == -1) { + maxResults = Integer.MAX_VALUE; + } + + if (firstResult > sessionTimestamps.size()) { + return Collections.emptyList(); + } + + int toIndex = (firstResult + maxResults) < sessionTimestamps.size() ? firstResult + maxResults : sessionTimestamps.size(); + sessionTimestamps = sessionTimestamps.subList(firstResult, toIndex); + } + + List userSessions = new LinkedList(); + for (Map.Entry e : sessionTimestamps) { + UserSessionEntity userSessionEntity = (UserSessionEntity) cache.get(e.getKey()); + if (userSessionEntity != null) { + userSessions.add(wrap(realm, userSessionEntity, offline)); + } + } + + return userSessions; + } + + @Override + public long getActiveUserSessions(RealmModel realm, ClientModel client) { + return getUserSessionsCount(realm, client, false); + } + + protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { + Cache cache = getCache(offline); + + Map map = new MapReduceTask(cache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) + .reducedWith(new LargestResultReducer()).execute(); + + return map.size(); + } + + @Override + public void removeUserSession(RealmModel realm, UserSessionModel session) { + removeUserSession(realm, session.getId()); + } + + @Override + public void removeUserSessions(RealmModel realm, UserModel user) { + removeUserSessions(realm, user, false); + } + + protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { + Cache cache = getCache(offline); + + Map sessions = new MapReduceTask(cache) + .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : sessions.keySet()) { + removeUserSession(realm, id, offline); + } + } + + @Override + public void removeExpired(RealmModel realm) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + + int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan(); + int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); + int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); + int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); + + Map map = new MapReduceTask(sessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).expired(expired, expiredRefresh).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + removeUserSession(realm, id); + } + + map = new MapReduceTask(sessionCache) + .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession(true).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(sessionCache, id); + } + + // Remove expired offline user sessions + Map map2 = new MapReduceTask(offlineSessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline)) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (Map.Entry entry : map2.entrySet()) { + String userSessionId = entry.getKey(); + tx.remove(offlineSessionCache, userSessionId); + // Propagate to persister + persister.removeUserSession(userSessionId, true); + + UserSessionEntity entity = (UserSessionEntity) entry.getValue(); + for (String clientSessionId : entity.getClientSessions()) { + tx.remove(offlineSessionCache, clientSessionId); + } + } + + // Remove expired offline client sessions + map = new MapReduceTask(offlineSessionCache) + .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredOffline).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String clientSessionId : map.keySet()) { + tx.remove(offlineSessionCache, clientSessionId); + persister.removeClientSession(clientSessionId, true); + } + + // Remove expired client initial access + map = new MapReduceTask(sessionCache) + .mappedWith(ClientInitialAccessMapper.create(realm.getId()).expired(Time.currentTime()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(sessionCache, id); + } + } + + @Override + public void removeUserSessions(RealmModel realm) { + removeUserSessions(realm, false); + } + + protected void removeUserSessions(RealmModel realm, boolean offline) { + Cache cache = getCache(offline); + + Map ids = new MapReduceTask(cache) + .mappedWith(SessionMapper.create(realm.getId()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : ids.keySet()) { + cache.remove(id); + } + } + + @Override + public void removeUserLoginFailure(RealmModel realm, String username) { + LoginFailureKey key = new LoginFailureKey(realm.getId(), username); + tx.remove(loginFailureCache, key); + } + + @Override + public void removeAllUserLoginFailures(RealmModel realm) { + Map sessions = new MapReduceTask(loginFailureCache) + .mappedWith(UserLoginFailureMapper.create(realm.getId()).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (LoginFailureKey id : sessions.keySet()) { + tx.remove(loginFailureCache, id); + } + } + + @Override + public void onRealmRemoved(RealmModel realm) { + removeUserSessions(realm, true); + removeUserSessions(realm, false); + removeAllUserLoginFailures(realm); + } + + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + onClientRemoved(realm, client, true); + onClientRemoved(realm, client, false); + } + + private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) { + Cache cache = getCache(offline); + + Map map = new MapReduceTask(cache) + .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (Map.Entry entry : map.entrySet()) { + + // detach from userSession + ClientSessionAdapter adapter = wrap(realm, entry.getValue(), offline); + adapter.setUserSession(null); + + tx.remove(cache, entry.getKey()); + } + } + + @Override + public void onUserRemoved(RealmModel realm, UserModel user) { + removeUserSessions(realm, user, true); + removeUserSessions(realm, user, false); + + loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getUsername())); + loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail())); + } + + @Override + public void removeClientSession(RealmModel realm, ClientSessionModel clientSession) { + removeClientSession(realm, clientSession, false); + } + + protected void removeClientSession(RealmModel realm, ClientSessionModel clientSession, boolean offline) { + Cache cache = getCache(offline); + + UserSessionModel userSession = clientSession.getUserSession(); + if (userSession != null) { + UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); + if (entity.getClientSessions() != null) { + entity.getClientSessions().remove(clientSession.getId()); + + } + tx.replace(cache, entity.getId(), entity); + } + tx.remove(cache, clientSession.getId()); + } + + protected void removeUserSession(RealmModel realm, String userSessionId) { + removeUserSession(realm, userSessionId, false); + } + + protected void removeUserSession(RealmModel realm, String userSessionId, boolean offline) { + Cache cache = getCache(offline); + + tx.remove(cache, userSessionId); + + // TODO: Isn't more effective to retrieve from userSessionEntity directly? + Map map = new MapReduceTask(cache) + .mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(cache, id); + } + } + + @Override + public void removeOfflineUserSession(RealmModel realm, String userSessionId) { + removeUserSession(realm, userSessionId, true); + } + + @Override + public List getOfflineClientSessions(RealmModel realm, UserModel user) { + Map sessions = new MapReduceTask(offlineSessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + + List clientSessions = new LinkedList<>(); + for (UserSessionEntity userSession : sessions.values()) { + Set currClientSessions = userSession.getClientSessions(); + for (String clientSessionId : currClientSessions) { + ClientSessionEntity cls = (ClientSessionEntity) offlineSessionCache.get(clientSessionId); + if (cls != null) { + clientSessions.add(cls); + } + } + } + + return wrapClientSessions(realm, clientSessions, true); + } + + @Override + public void removeOfflineClientSession(RealmModel realm, String clientSessionId) { + ClientSessionModel clientSession = getOfflineClientSession(realm, clientSessionId); + removeClientSession(realm, clientSession, true); + } + + @Override + public long getOfflineSessionsCount(RealmModel realm, ClientModel client) { + return getUserSessionsCount(realm, client, true); + } + + @Override + public List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) { + return getUserSessions(realm, client, first, max, true); + } + + @Override + public List listClientInitialAccess(RealmModel realm) { + Map entities = new MapReduceTask(sessionCache) + .mappedWith(ClientInitialAccessMapper.create(realm.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + return wrapClientInitialAccess(realm, entities.values()); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java new file mode 100644 index 0000000000..0eed3308c4 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java @@ -0,0 +1,66 @@ +package org.keycloak.models.sessions.infinispan; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * @author Stian Thorgersen + */ +public class Consumers { + + private Consumers() { + } + + public static UserSessionModelsConsumer userSessionModels(InfinispanUserSessionProvider provider, RealmModel realm, boolean offline) { + return new UserSessionModelsConsumer(provider, realm, offline); + } + + public static class UserSessionIdAndTimestampConsumer implements Consumer> { + + private Map sessions = new HashMap<>(); + + @Override + public void accept(Map.Entry entry) { + SessionEntity e = entry.getValue(); + if (e instanceof ClientSessionEntity) { + ClientSessionEntity ce = (ClientSessionEntity) e; + sessions.put(ce.getUserSession(), ce.getTimestamp()); + } + } + + } + + public static class UserSessionModelsConsumer implements Consumer> { + + private InfinispanUserSessionProvider provider; + private RealmModel realm; + private boolean offline; + private List sessions = new LinkedList<>(); + + private UserSessionModelsConsumer(InfinispanUserSessionProvider provider, RealmModel realm, boolean offline) { + this.provider = provider; + this.realm = realm; + this.offline = offline; + } + + @Override + public void accept(Map.Entry entry) { + SessionEntity e = entry.getValue(); + sessions.add(provider.wrap(realm, (UserSessionEntity) e, offline)); + } + + public List getSessions() { + return sessions; + } + + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index d89f47b023..7e66b12b2a 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -1,25 +1,21 @@ package org.keycloak.models.sessions.infinispan; import org.infinispan.Cache; -import org.infinispan.distexec.mapreduce.MapReduceTask; +import org.infinispan.CacheStream; import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; import org.keycloak.models.*; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.entities.*; -import org.keycloak.models.sessions.infinispan.mapreduce.*; +import org.keycloak.models.sessions.infinispan.stream.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RealmInfoUtil; -import org.keycloak.common.util.Time; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Stian Thorgersen @@ -28,11 +24,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class); - private final KeycloakSession session; - private final Cache sessionCache; - private final Cache offlineSessionCache; - private final Cache loginFailureCache; - private final InfinispanKeycloakTransaction tx; + protected final KeycloakSession session; + protected final Cache sessionCache; + protected final Cache offlineSessionCache; + protected final Cache loginFailureCache; + protected final InfinispanKeycloakTransaction tx; public InfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache offlineSessionCache, Cache loginFailureCache) { @@ -139,36 +135,31 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return wrap(realm, entity, offline); } - @Override - public List getUserSessions(RealmModel realm, UserModel user) { - Map sessions = new MapReduceTask(sessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId())) - .reducedWith(new FirstResultReducer()) - .execute(); + protected List getUserSessions(RealmModel realm, Predicate> predicate, boolean offline) { + CacheStream> cacheStream = getCache(offline).entrySet().stream(); + Iterator> itr = cacheStream.filter(predicate).iterator(); + List sessions = new LinkedList<>(); + while (itr.hasNext()) { + UserSessionEntity e = (UserSessionEntity) itr.next().getValue(); + sessions.add(wrap(realm, e, offline)); + } + return sessions; + } - return wrapUserSessions(realm, sessions.values(), false); + @Override + public List getUserSessions(final RealmModel realm, UserModel user) { + return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), false); } @Override public List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId) { - Map sessions = new MapReduceTask(sessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).brokerUserId(brokerUserId)) - .reducedWith(new FirstResultReducer()) - .execute(); - - return wrapUserSessions(realm, sessions.values(), false); + return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).brokerUserId(brokerUserId), false); } @Override public UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) { - Map sessions = new MapReduceTask(sessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).brokerSessionId(brokerSessionId)) - .reducedWith(new FirstResultReducer()) - .execute(); - - List userSessionModels = wrapUserSessions(realm, sessions.values(), false); - if (userSessionModels.isEmpty()) return null; - return userSessionModels.get(0); + List userSessions = getUserSessions(realm, UserSessionPredicate.create(realm.getId()).brokerSessionId(brokerSessionId), false); + return userSessions.isEmpty() ? null : userSessions.get(0); } @Override @@ -181,86 +172,58 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return getUserSessions(realm, client, firstResult, maxResults, false); } - protected List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults, boolean offline) { - Cache cache = getCache(offline); + protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { + final Cache cache = getCache(offline); - Map map = new MapReduceTask(cache) - .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) - .reducedWith(new LargestResultReducer()) - .execute(); + Iterator itr = cache.entrySet().stream() + .filter(ClientSessionPredicate.create(realm.getId()).client(client.getId()).requireUserSession()) + .map(Mappers.clientSessionToUserSessionTimestamp()) + .iterator(); - List> sessionTimestamps = new LinkedList>(map.entrySet()); + Map m = new HashMap<>(); + while(itr.hasNext()) { + UserSessionTimestamp next = itr.next(); + if (!m.containsKey(next.getUserSessionId()) || m.get(next.getUserSessionId()).getClientSessionTimestamp() < next.getClientSessionTimestamp()) { + m.put(next.getUserSessionId(), next); + } + } - Collections.sort(sessionTimestamps, new Comparator>() { + Stream stream = new LinkedList<>(m.values()).stream().sorted(Comparators.userSessionTimestamp()); + + if (firstResult > 0) { + stream = stream.skip(firstResult); + } + + if (maxResults > 0) { + stream = stream.limit(maxResults); + } + + final List sessions = new LinkedList<>(); + stream.forEach(new Consumer() { @Override - public int compare(Map.Entry e1, Map.Entry e2) { - return e1.getValue().compareTo(e2.getValue()); + public void accept(UserSessionTimestamp userSessionTimestamp) { + SessionEntity entity = cache.get(userSessionTimestamp.getUserSessionId()); + if (entity != null) { + sessions.add(wrap(realm, (UserSessionEntity) entity, offline)); + } } }); - if (firstResult != -1 || maxResults == -1) { - if (firstResult == -1) { - firstResult = 0; - } - - if (maxResults == -1) { - maxResults = Integer.MAX_VALUE; - } - - if (firstResult > sessionTimestamps.size()) { - return Collections.emptyList(); - } - - int toIndex = (firstResult + maxResults) < sessionTimestamps.size() ? firstResult + maxResults : sessionTimestamps.size(); - sessionTimestamps = sessionTimestamps.subList(firstResult, toIndex); - } - - List userSessions = new LinkedList(); - for (Map.Entry e : sessionTimestamps) { - UserSessionEntity userSessionEntity = (UserSessionEntity) cache.get(e.getKey()); - if (userSessionEntity != null) { - userSessions.add(wrap(realm, userSessionEntity, offline)); - } - } - - return userSessions; + return sessions; } @Override - public List getUserSessionsByNote(RealmModel realm, String noteName, String noteValue) { - HashMap notes = new HashMap<>(); - notes.put(noteName, noteValue); - return getUserSessionsByNotes(realm, notes); - } - - public List getUserSessionsByNotes(RealmModel realm, Map notes) { - Map sessions = new MapReduceTask(sessionCache) - .mappedWith(UserSessionNoteMapper.create(realm.getId()).notes(notes)) - .reducedWith(new FirstResultReducer()) - .execute(); - - return wrapUserSessions(realm, sessions.values(), false); - - } - - @Override - public int getActiveUserSessions(RealmModel realm, ClientModel client) { + public long getActiveUserSessions(RealmModel realm, ClientModel client) { return getUserSessionsCount(realm, client, false); } - protected int getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { - Cache cache = getCache(offline); - - Map map = new MapReduceTask(cache) - .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp()) - .reducedWith(new LargestResultReducer()).execute(); - - return map.size(); + protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { + return getCache(offline).entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).client(client.getId()).requireUserSession()).map(Mappers.clientSessionToUserSessionId()).distinct().count(); } @Override public void removeUserSession(RealmModel realm, UserSessionModel session) { - removeUserSession(realm, session.getId()); + removeUserSession(realm, session.getId(), false); } @Override @@ -271,80 +234,81 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { Cache cache = getCache(offline); - Map sessions = new MapReduceTask(cache) - .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (String id : sessions.keySet()) { - removeUserSession(realm, id, offline); + Iterator itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.sessionId()).iterator(); + while (itr.hasNext()) { + removeUserSession(realm, itr.next(), offline); } } @Override - public void removeExpiredUserSessions(RealmModel realm) { - UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + public void removeExpired(RealmModel realm) { + removeExpiredUserSessions(realm); + removeExpiredClientSessions(realm); + removeExpiredOfflineUserSessions(realm); + removeExpiredOfflineClientSessions(realm); + removeExpiredClientInitialAccess(realm); + } + private void removeExpiredUserSessions(RealmModel realm) { int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); - int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); + + Iterator> itr = sessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator(); + + while (itr.hasNext()) { + UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); + tx.remove(sessionCache, entity.getId()); + + if (entity.getClientSessions() != null) { + for (String clientSessionId : entity.getClientSessions()) { + tx.remove(sessionCache, clientSessionId); + } + } + } + } + + private void removeExpiredClientSessions(RealmModel realm) { int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); - Map map = new MapReduceTask(sessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).expired(expired, expiredRefresh).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (String id : map.keySet()) { - removeUserSession(realm, id); + Iterator> itr = sessionCache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator(); + while (itr.hasNext()) { + tx.remove(sessionCache, itr.next().getKey()); } + } - map = new MapReduceTask(sessionCache) - .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession(true).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); + private void removeExpiredOfflineUserSessions(RealmModel realm) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); - for (String id : map.keySet()) { - tx.remove(sessionCache, id); - } + Iterator> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline)).iterator(); + while (itr.hasNext()) { + UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); + tx.remove(offlineSessionCache, entity.getId()); - // Remove expired offline user sessions - Map map2 = new MapReduceTask(offlineSessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline)) - .reducedWith(new FirstResultReducer()) - .execute(); + persister.removeUserSession(entity.getId(), true); - for (Map.Entry entry : map2.entrySet()) { - String userSessionId = entry.getKey(); - tx.remove(offlineSessionCache, userSessionId); - // Propagate to persister - persister.removeUserSession(userSessionId, true); - - UserSessionEntity entity = (UserSessionEntity) entry.getValue(); for (String clientSessionId : entity.getClientSessions()) { tx.remove(offlineSessionCache, clientSessionId); } } + } - // Remove expired offline client sessions - map = new MapReduceTask(offlineSessionCache) - .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredOffline).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); + private void removeExpiredOfflineClientSessions(RealmModel realm) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); - for (String clientSessionId : map.keySet()) { - tx.remove(offlineSessionCache, clientSessionId); - persister.removeClientSession(clientSessionId, true); + Iterator itr = offlineSessionCache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator(); + while (itr.hasNext()) { + String sessionId = itr.next(); + tx.remove(offlineSessionCache, sessionId); + persister.removeClientSession(sessionId, true); } + } - // Remove expired client initial access - map = new MapReduceTask(sessionCache) - .mappedWith(ClientInitialAccessMapper.create(realm.getId()).expired(Time.currentTime()).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (String id : map.keySet()) { - tx.remove(sessionCache, id); + private void removeExpiredClientInitialAccess(RealmModel realm) { + Iterator itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator(); + while (itr.hasNext()) { + tx.remove(sessionCache, itr.next()); } } @@ -356,13 +320,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected void removeUserSessions(RealmModel realm, boolean offline) { Cache cache = getCache(offline); - Map ids = new MapReduceTask(cache) - .mappedWith(SessionMapper.create(realm.getId()).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (String id : ids.keySet()) { - cache.remove(id); + Iterator itr = cache.entrySet().stream().filter(SessionPredicate.create(realm.getId())).map(Mappers.sessionId()).iterator(); + while (itr.hasNext()) { + cache.remove(itr.next()); } } @@ -384,24 +344,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void removeUserLoginFailure(RealmModel realm, String username) { - LoginFailureKey key = new LoginFailureKey(realm.getId(), username); - tx.remove(loginFailureCache, key); + tx.remove(loginFailureCache, new LoginFailureKey(realm.getId(), username)); } @Override public void removeAllUserLoginFailures(RealmModel realm) { - Map sessions = new MapReduceTask(loginFailureCache) - .mappedWith(UserLoginFailureMapper.create(realm.getId()).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (LoginFailureKey id : sessions.keySet()) { - tx.remove(loginFailureCache, id); + Iterator itr = loginFailureCache.entrySet().stream().filter(UserLoginFailurePredicate.create(realm.getId())).map(Mappers.loginFailureId()).iterator(); + while (itr.hasNext()) { + LoginFailureKey key = itr.next(); + tx.remove(loginFailureCache, key); } } - - @Override public void onRealmRemoved(RealmModel realm) { removeUserSessions(realm, true); @@ -418,18 +372,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) { Cache cache = getCache(offline); - Map map = new MapReduceTask(cache) - .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId())) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (Map.Entry entry : map.entrySet()) { - - // detach from userSession - ClientSessionAdapter adapter = wrap(realm, entry.getValue(), offline); + Iterator> itr = cache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).client(client.getId())).iterator(); + while (itr.hasNext()) { + ClientSessionEntity entity = (ClientSessionEntity) itr.next().getValue(); + ClientSessionAdapter adapter = wrap(realm, entity, offline); adapter.setUserSession(null); - tx.remove(cache, entry.getKey()); + tx.remove(cache, entity.getId()); } } @@ -491,27 +440,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } } - protected void removeUserSession(RealmModel realm, String userSessionId) { - removeUserSession(realm, userSessionId, false); - } - protected void removeUserSession(RealmModel realm, String userSessionId, boolean offline) { Cache cache = getCache(offline); tx.remove(cache, userSessionId); - // TODO: Isn't more effective to retrieve from userSessionEntity directly? - Map map = new MapReduceTask(cache) - .mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey()) - .reducedWith(new FirstResultReducer()) - .execute(); - - for (String id : map.keySet()) { - tx.remove(cache, id); + UserSessionEntity sessionEntity = (UserSessionEntity) cache.get(userSessionId); + if (sessionEntity.getClientSessions() != null) { + for (String clientSessionId : sessionEntity.getClientSessions()) { + tx.remove(cache, clientSessionId); + } } } - InfinispanKeycloakTransaction getTx() { return tx; } @@ -522,7 +463,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } List wrapUserSessions(RealmModel realm, Collection entities, boolean offline) { - List models = new LinkedList(); + List models = new LinkedList<>(); for (UserSessionEntity e : entities) { models.add(wrap(realm, e, offline)); } @@ -553,7 +494,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } List wrapClientSessions(RealmModel realm, Collection entities, boolean offline) { - List models = new LinkedList(); + List models = new LinkedList<>(); for (ClientSessionEntity e : entities) { models.add(wrap(realm, e, offline)); } @@ -600,23 +541,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public List getOfflineClientSessions(RealmModel realm, UserModel user) { - Map sessions = new MapReduceTask(offlineSessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId())) - .reducedWith(new FirstResultReducer()) - .execute(); + Iterator> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator(); + List clientSessions = new LinkedList<>(); - List clientSessions = new LinkedList<>(); - for (UserSessionEntity userSession : sessions.values()) { - Set currClientSessions = userSession.getClientSessions(); + while(itr.hasNext()) { + UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); + Set currClientSessions = entity.getClientSessions(); for (String clientSessionId : currClientSessions) { ClientSessionEntity cls = (ClientSessionEntity) offlineSessionCache.get(clientSessionId); if (cls != null) { - clientSessions.add(cls); + clientSessions.add(wrap(realm, cls, true)); } } } - return wrapClientSessions(realm, clientSessions, true); + return clientSessions; } @Override @@ -626,7 +565,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public int getOfflineSessionsCount(RealmModel realm, ClientModel client) { + public long getOfflineSessionsCount(RealmModel realm, ClientModel client) { return getUserSessionsCount(realm, client, true); } @@ -721,18 +660,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public List listClientInitialAccess(RealmModel realm) { - Map entities = new MapReduceTask(sessionCache) - .mappedWith(ClientInitialAccessMapper.create(realm.getId())) - .reducedWith(new FirstResultReducer()) - .execute(); - return wrapClientInitialAccess(realm, entities.values()); + Iterator> itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId())).iterator(); + List list = new LinkedList<>(); + while (itr.hasNext()) { + list.add(wrap(realm, (ClientInitialAccessEntity) itr.next().getValue())); + } + return list; } class InfinispanKeycloakTransaction implements KeycloakTransaction { private boolean active; private boolean rollback; - private Map tasks = new HashMap(); + private Map tasks = new HashMap<>(); @Override public void begin() { diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index 382d01f630..e18aa847e0 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -47,7 +47,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider Cache cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); Cache offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); Cache loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); - return new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures); + + return isStreamMode() ? new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures) : new CompatInfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures); } else { return compatProviderFactory.create(session); } @@ -147,5 +148,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider return false; } + private boolean isStreamMode() { + return Version.getVersionShort() >= Version.getVersionShort("8.1.0.Final"); + } + } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionTimestamp.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionTimestamp.java new file mode 100644 index 0000000000..3284e43158 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionTimestamp.java @@ -0,0 +1,24 @@ +package org.keycloak.models.sessions.infinispan; + +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +public class UserSessionTimestamp implements Serializable { + private String userSessionId; + private int clientSessionTimestamp; + + public UserSessionTimestamp(String userSessionId, int clientSessionTimestamp) { + this.userSessionId = userSessionId; + this.clientSessionTimestamp = clientSessionTimestamp; + } + + public String getUserSessionId() { + return userSessionId; + } + + public int getClientSessionTimestamp() { + return clientSessionTimestamp; + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java index db20ef8940..693db6718c 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java @@ -176,17 +176,6 @@ public class MemUserSessionProvider implements UserSessionProvider { return userSessions; } - @Override - public List getUserSessionsByNote(RealmModel realm, String noteName, String noteValue) { - List userSessions = new LinkedList(); - for (UserSessionEntity s : this.userSessions.values()) { - if (s.getRealm().equals(realm.getId()) && noteValue.equals(s.getNotes().get(noteName))) { - userSessions.add(new UserSessionAdapter(session, this, realm, s)); - } - } - return userSessions; - } - @Override public List getUserSessions(RealmModel realm, ClientModel client) { return getUserSessions(realm, client, false); @@ -230,7 +219,7 @@ public class MemUserSessionProvider implements UserSessionProvider { } @Override - public int getActiveUserSessions(RealmModel realm, ClientModel client) { + public long getActiveUserSessions(RealmModel realm, ClientModel client) { return getUserSessions(realm, client, false).size(); } @@ -287,7 +276,7 @@ public class MemUserSessionProvider implements UserSessionProvider { } @Override - public void removeExpiredUserSessions(RealmModel realm) { + public void removeExpired(RealmModel realm) { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); Iterator itr = userSessions.values().iterator(); @@ -565,7 +554,7 @@ public class MemUserSessionProvider implements UserSessionProvider { } @Override - public int getOfflineSessionsCount(RealmModel realm, ClientModel client) { + public long getOfflineSessionsCount(RealmModel realm, ClientModel client) { return getUserSessions(realm, client, true).size(); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java index b368fd3811..2a9058d643 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java @@ -151,13 +151,14 @@ public class InfinispanUserSessionInitializer { int processors = Runtime.getRuntime().availableProcessors(); ExecutorService localExecutor = Executors.newCachedThreadPool(); - DistributedExecutorService distributedExecutorService = new DefaultExecutorService(cache, localExecutor); + Transport transport = cache.getCacheManager().getTransport(); + boolean distributed = transport != null; + ExecutorService executorService = distributed ? new DefaultExecutorService(cache, localExecutor) : localExecutor; int errors = 0; try { while (!state.isFinished()) { - Transport transport = cache.getCacheManager().getTransport(); int nodesCount = transport==null ? 1 : transport.getMembers().size(); int distributedWorkersCount = processors * nodesCount; @@ -173,8 +174,11 @@ public class InfinispanUserSessionInitializer { for (Integer segment : segments) { SessionInitializerWorker worker = new SessionInitializerWorker(); worker.setWorkerEnvironment(segment, sessionsPerSegment, sessionLoader); + if (!distributed) { + worker.setEnvironment(cache, null); + } - Future future = distributedExecutorService.submit(worker); + Future future = executorService.submit(worker); futures.add(future); } @@ -210,7 +214,9 @@ public class InfinispanUserSessionInitializer { } } } finally { - distributedExecutorService.shutdown(); + if (distributed) { + executorService.shutdown(); + } localExecutor.shutdown(); } } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientInitialAccessPredicate.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientInitialAccessPredicate.java new file mode 100644 index 0000000000..22518ef679 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientInitialAccessPredicate.java @@ -0,0 +1,59 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessPredicate implements Predicate>, Serializable { + + public ClientInitialAccessPredicate(String realm) { + this.realm = realm; + } + + private String realm; + + private Integer expired; + + public static ClientInitialAccessPredicate create(String realm) { + return new ClientInitialAccessPredicate(realm); + } + + public ClientInitialAccessPredicate expired(int time) { + this.expired = time; + return this; + } + + @Override + public boolean test(Map.Entry entry) { + SessionEntity e = entry.getValue(); + + if (!realm.equals(e.getRealm())) { + return false; + } + + if (!(e instanceof ClientInitialAccessEntity)) { + return false; + } + + ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e; + + if (expired != null) { + if (entity.getRemainingCount() <= 0) { + return true; + } else if (entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < expired) { + return true; + } else { + return false; + } + } + + return true; + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java new file mode 100644 index 0000000000..490b8f4906 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java @@ -0,0 +1,97 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionPredicate implements Predicate>, Serializable { + + private String realm; + + private String client; + + private String userSession; + + private Long expiredRefresh; + + private Boolean requireUserSession = false; + + private Boolean requireNullUserSession = false; + + private ClientSessionPredicate(String realm) { + this.realm = realm; + } + + public static ClientSessionPredicate create(String realm) { + return new ClientSessionPredicate(realm); + } + + public ClientSessionPredicate client(String client) { + this.client = client; + return this; + } + + public ClientSessionPredicate userSession(String userSession) { + this.userSession = userSession; + return this; + } + + public ClientSessionPredicate expiredRefresh(long expiredRefresh) { + this.expiredRefresh = expiredRefresh; + return this; + } + + public ClientSessionPredicate requireUserSession() { + requireUserSession = true; + return this; + } + + public ClientSessionPredicate requireNullUserSession() { + requireNullUserSession = true; + return this; + } + + @Override + public boolean test(Map.Entry entry) { + SessionEntity e = entry.getValue(); + + if (!realm.equals(e.getRealm())) { + return false; + } + + if (!(e instanceof ClientSessionEntity)) { + return false; + } + + ClientSessionEntity entity = (ClientSessionEntity) e; + + if (client != null && !entity.getClient().equals(client)) { + return false; + } + + if (userSession != null && !userSession.equals(entity.getUserSession())) { + return false; + } + + if (requireUserSession && entity.getUserSession() == null) { + return false; + } + + if (requireNullUserSession && entity.getUserSession() != null) { + return false; + } + + if (expiredRefresh != null && entity.getTimestamp() > expiredRefresh) { + return false; + } + + return true; + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java new file mode 100644 index 0000000000..ba7fd0234d --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java @@ -0,0 +1,24 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.UserSessionTimestamp; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * @author Stian Thorgersen + */ +public class Comparators { + + public static Comparator userSessionTimestamp() { + return new UserSessionTimestampComparator(); + } + + private static class UserSessionTimestampComparator implements Comparator, Serializable { + @Override + public int compare(UserSessionTimestamp u1, UserSessionTimestamp u2) { + return u1.getClientSessionTimestamp() - u2.getClientSessionTimestamp(); + } + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java new file mode 100644 index 0000000000..3164f6dbfb --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java @@ -0,0 +1,77 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.UserSessionTimestamp; +import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +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 java.io.Serializable; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * @author Stian Thorgersen + */ +public class Mappers { + + public static Function, UserSessionTimestamp> clientSessionToUserSessionTimestamp() { + return new ClientSessionToUserSessionTimestampMapper(); + } + + public static Function>, UserSessionTimestamp> userSessionTimestamp() { + return new UserSessionTimestampMapper(); + } + + public static Function, String> sessionId() { + return new SessionIdMapper(); + } + + public static Function, LoginFailureKey> loginFailureId() { + return new LoginFailureIdMapper(); + } + + public static Function, String> clientSessionToUserSessionId() { + return new ClientSessionToUserSessionIdMapper(); + } + + private static class ClientSessionToUserSessionTimestampMapper implements Function, UserSessionTimestamp>, Serializable { + @Override + public UserSessionTimestamp apply(Map.Entry entry) { + SessionEntity e = entry.getValue(); + ClientSessionEntity entity = (ClientSessionEntity) e; + return new UserSessionTimestamp(entity.getUserSession(), entity.getTimestamp()); + } + } + + private static class UserSessionTimestampMapper implements Function>, org.keycloak.models.sessions.infinispan.UserSessionTimestamp>, Serializable { + @Override + public org.keycloak.models.sessions.infinispan.UserSessionTimestamp apply(Map.Entry> e) { + return e.getValue().get(); + } + } + + private static class SessionIdMapper implements Function, String>, Serializable { + @Override + public String apply(Map.Entry entry) { + return entry.getKey(); + } + } + + private static class LoginFailureIdMapper implements Function, LoginFailureKey>, Serializable { + @Override + public LoginFailureKey apply(Map.Entry entry) { + return entry.getKey(); + } + } + + private static class ClientSessionToUserSessionIdMapper implements Function, String>, Serializable { + @Override + public String apply(Map.Entry entry) { + SessionEntity e = entry.getValue(); + ClientSessionEntity entity = (ClientSessionEntity) e; + return entity.getUserSession(); + } + } +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java new file mode 100755 index 0000000000..6dd5540b3d --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java @@ -0,0 +1,29 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +public class SessionPredicate implements Predicate>, Serializable { + + private String realm; + + private SessionPredicate(String realm) { + this.realm = realm; + } + + public static SessionPredicate create(String realm) { + return new SessionPredicate(realm); + } + + @Override + public boolean test(Map.Entry entry) { + return realm.equals(entry.getValue().getRealm()); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java new file mode 100755 index 0000000000..fb0fb7ecab --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java @@ -0,0 +1,31 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; +import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +public class UserLoginFailurePredicate implements Predicate>, Serializable { + + private String realm; + + private UserLoginFailurePredicate(String realm) { + this.realm = realm; + } + + public static UserLoginFailurePredicate create(String realm) { + return new UserLoginFailurePredicate(realm); + } + + @Override + public boolean test(Map.Entry entry) { + LoginFailureEntity e = entry.getValue(); + return realm.equals(e.getRealm()); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java new file mode 100644 index 0000000000..2add0492dd --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java @@ -0,0 +1,91 @@ +package org.keycloak.models.sessions.infinispan.stream; + +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; + +/** + * @author Stian Thorgersen + */ +public class UserSessionPredicate implements Predicate>, Serializable { + + private String realm; + + private String user; + + private Integer expired; + + private Integer expiredRefresh; + + private String brokerSessionId; + private String brokerUserId; + + private UserSessionPredicate(String realm) { + this.realm = realm; + } + + public static UserSessionPredicate create(String realm) { + return new UserSessionPredicate(realm); + } + + public UserSessionPredicate user(String user) { + this.user = user; + return this; + } + + public UserSessionPredicate expired(Integer expired, Integer expiredRefresh) { + this.expired = expired; + this.expiredRefresh = expiredRefresh; + return this; + } + + public UserSessionPredicate brokerSessionId(String id) { + this.brokerSessionId = id; + return this; + } + + public UserSessionPredicate brokerUserId(String id) { + this.brokerUserId = id; + return this; + } + + @Override + public boolean test(Map.Entry entry) { + SessionEntity e = entry.getValue(); + + if (!(e instanceof UserSessionEntity)) { + return false; + } + + UserSessionEntity entity = (UserSessionEntity) e; + + if (!realm.equals(entity.getRealm())) { + return false; + } + + if (user != null && !entity.getUser().equals(user)) { + return false; + } + + if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) { + return false; + } + + if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) { + return false; + } + + if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) { + return false; + } + + if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) { + return false; + } + + return true; + } +} diff --git a/pom.xml b/pom.xml index b2aa2f416b..03e1416a39 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.jboss jboss-parent - 16 + 19 Keycloak @@ -49,8 +49,8 @@ 1.6.1 1.4.01 1.7.7 - 10.0.0.CR4 - 2.0.1.Final + 10.0.0.CR5 + 2.0.5.Final 1.1.0.Final @@ -66,7 +66,7 @@ 2.2.11 20140925 1.4.5 - 6.0.2.Final + 8.1.0.Final 3.4.1 9.1.0.v20131115 4.2.0 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 03b06360e8..73ebe4448d 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 @@ -383,9 +383,9 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Map getApplicationSessionCount() { + public Map getApplicationSessionCount() { auth.requireView(); - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("count", session.sessions().getActiveUserSessions(client.getRealm(), client)); return map; } @@ -430,9 +430,9 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Map getOfflineSessionCount() { + public Map getOfflineSessionCount() { auth.requireView(); - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("count", session.sessions().getOfflineSessionsCount(client.getRealm(), client)); return map; } 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 9d144e4c2d..cb97855f02 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -369,9 +369,9 @@ public class RealmAdminResource { auth.requireView(); List> data = new LinkedList>(); for (ClientModel client : realm.getClients()) { - int size = session.sessions().getActiveUserSessions(client.getRealm(), client); + long size = session.sessions().getActiveUserSessions(client.getRealm(), client); if (size == 0) continue; - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("id", client.getId()); map.put("clientId", client.getClientId()); map.put("active", size + ""); diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java index 96c8baded6..8f6b91c457 100755 --- a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java +++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java @@ -13,7 +13,7 @@ public class ClearExpiredUserSessions implements ScheduledTask { public void run(KeycloakSession session) { UserSessionProvider sessions = session.sessions(); for (RealmModel realm : session.realms().getRealms()) { - sessions.removeExpiredUserSessions(realm); + sessions.removeExpired(realm); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java index 5d02107f5f..08529ca5c5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java @@ -100,6 +100,8 @@ public class AdapterTest { @Test public void testLoginSSOAndLogout() throws Exception { + testStrategy.testLoginSSOMax(); + testStrategy.testLoginSSOAndLogout(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java index 1e6009674a..f93c4ebf87 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java @@ -310,7 +310,7 @@ public class AdapterTestStrategy extends ExternalResource { session = keycloakRule.startSession(); realm = session.realms().getRealmByName("demo"); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); session.getTransaction().commit(); session.close(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java index 4d438972f8..19beaf6edf 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java @@ -286,9 +286,6 @@ public class BruteForceTest { } - - - @Test public void testBrowserInvalidPassword() throws Exception { loginSuccess(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 30d94fae78..c12e6feb58 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -524,7 +524,7 @@ public class LoginTest { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - manager.getSession().sessions().removeExpiredUserSessions(appRealm); + manager.getSession().sessions().removeExpired(appRealm); } }); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index ab93b1c663..147ee8619f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -347,7 +347,7 @@ public class UserSessionProviderOfflineTest { resetSession(); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); @@ -372,7 +372,7 @@ public class UserSessionProviderOfflineTest { // Expire everything and assert nothing found Time.setOffset(3000000); try { - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); 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 88e7597ddc..81064583f7 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 @@ -17,11 +17,7 @@ import org.keycloak.services.managers.UserManager; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.common.util.Time; -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; import static org.junit.Assert.*; @@ -276,7 +272,7 @@ public class UserSessionProviderTest { resetSession(); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); for (String e : expired) { @@ -309,13 +305,13 @@ public class UserSessionProviderTest { resetSession(); Time.setOffset(25); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); assertNotNull(session.sessions().getClientSession(clientSessionId)); Time.setOffset(35); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); assertNull(session.sessions().getClientSession(clientSessionId)); @@ -328,13 +324,13 @@ public class UserSessionProviderTest { resetSession(); Time.setOffset(35); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); assertNotNull(session.sessions().getClientSession(clientSessionId)); Time.setOffset(45); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); assertNull(session.sessions().getClientSession(clientSessionId)); @@ -347,13 +343,13 @@ public class UserSessionProviderTest { resetSession(); Time.setOffset(45); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); assertNotNull(session.sessions().getClientSession(clientSessionId)); Time.setOffset(55); - session.sessions().removeExpiredUserSessions(realm); + session.sessions().removeExpired(realm); resetSession(); assertNull(session.sessions().getClientSession(clientSessionId)); @@ -463,6 +459,18 @@ public class UserSessionProviderTest { failure1 = session.sessions().getUserLoginFailure(realm, "user1"); assertEquals(0, failure1.getNumFailures()); + + session.sessions().removeUserLoginFailure(realm, "user1"); + + resetSession(); + + assertNull(session.sessions().getUserLoginFailure(realm, "user1")); + + session.sessions().removeAllUserLoginFailures(realm); + + resetSession(); + + assertNull(session.sessions().getUserLoginFailure(realm, "user2")); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index caa61edefd..1af7515dc3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -263,7 +263,7 @@ public class OfflineTokenTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - manager.getSession().sessions().removeExpiredUserSessions(appRealm); + manager.getSession().sessions().removeExpired(appRealm); } }); From c684dac625f8035604ddf6568b3476b81677ce8a Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 6 Jan 2016 14:43:10 +0100 Subject: [PATCH 58/65] Tweak mem for Travis --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b2b362762e..3da0679c50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,18 @@ language: java +env: + global: + - MAVEN_SKIP_RC=true + - MAVEN_OPTS="-Xms512m -Xmx2048m" + jdk: - oraclejdk8 +before_script: + - export MAVEN_SKIP_RC=true + install: - - travis_wait mvn install -Pdistribution -DskipTests=true -B -V -q + - mvn install -Pdistribution -DskipTests=true -B -V -q script: - mvn test -B From 64de96d34b3b3e6160e7e1bbbbfffa872dee82b1 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 6 Jan 2016 16:49:58 -0500 Subject: [PATCH 59/65] installation provider --- .../ClientInstallationRepresentation.java | 71 ++++++++++ .../info/ServerInfoRepresentation.java | 9 ++ .../theme/base/admin/resources/js/app.js | 3 + .../admin/resources/js/controllers/clients.js | 27 +++- .../theme/base/admin/resources/js/loaders.js | 9 -- .../theme/base/admin/resources/js/services.js | 16 ++- .../partials/client-installation.html | 2 +- .../protocol/ClientInstallationProvider.java | 27 ++++ .../protocol/ClientInstallationSpi.java | 32 +++++ .../KeycloakOIDCClientInstallation.java | 129 ++++++++++++++++++ ...kOIDCJbossSubsystemClientInstallation.java | 117 ++++++++++++++++ .../resources/admin/ClientResource.java | 10 ++ .../admin/info/ServerInfoAdminResource.java | 23 ++++ ...ycloak.protocol.ClientInstallationProvider | 2 + .../services/org.keycloak.provider.Spi | 1 + .../keycloak/testsuite/admin/RealmTest.java | 1 + 16 files changed, 461 insertions(+), 18 deletions(-) create mode 100755 core/src/main/java/org/keycloak/representations/info/ClientInstallationRepresentation.java mode change 100644 => 100755 core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java create mode 100755 services/src/main/java/org/keycloak/protocol/ClientInstallationProvider.java create mode 100755 services/src/main/java/org/keycloak/protocol/ClientInstallationSpi.java create mode 100755 services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java create mode 100755 services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java create mode 100755 services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider diff --git a/core/src/main/java/org/keycloak/representations/info/ClientInstallationRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ClientInstallationRepresentation.java new file mode 100755 index 0000000000..051229e16c --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/info/ClientInstallationRepresentation.java @@ -0,0 +1,71 @@ +package org.keycloak.representations.info; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientInstallationRepresentation { + protected String id; + protected String protocol; + protected boolean downloadOnly; + protected String displayType; + protected String helpText; + protected String filename; + protected String mediaType; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public boolean isDownloadOnly() { + return downloadOnly; + } + + public void setDownloadOnly(boolean downloadOnly) { + this.downloadOnly = downloadOnly; + } + + public String getDisplayType() { + return displayType; + } + + public void setDisplayType(String displayType) { + this.displayType = displayType; + } + + public String getHelpText() { + return helpText; + } + + public void setHelpText(String helpText) { + this.helpText = helpText; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } +} diff --git a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java old mode 100644 new mode 100755 index 01547901f1..640f3f6119 --- a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java @@ -24,6 +24,7 @@ public class ServerInfoRepresentation { private Map> protocolMapperTypes; private Map> builtinProtocolMappers; + private Map> clientInstallations; private Map> enums; @@ -105,4 +106,12 @@ public class ServerInfoRepresentation { public void setEnums(Map> enums) { this.enums = enums; } + + public Map> getClientInstallations() { + return clientInstallations; + } + + public void setClientInstallations(Map> clientInstallations) { + this.clientInstallations = clientInstallations; + } } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 65693881f9..be30adfe79 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1109,6 +1109,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, client : function(ClientLoader) { return ClientLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller : 'ClientInstallationCtrl' diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 14243077af..f80e57a3dd 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -685,7 +685,7 @@ module.controller('ClientListCtrl', function($scope, realm, clients, Client, ser }; }); -module.controller('ClientInstallationCtrl', function($scope, realm, client, ClientInstallation,ClientInstallationJBoss, $http, $routeParams) { +module.controller('ClientInstallationCtrl', function($scope, realm, client, serverInfo, ClientInstallation,$http, $routeParams) { $scope.realm = realm; $scope.client = client; $scope.installation = null; @@ -693,11 +693,25 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, Clie $scope.configFormat = null; $scope.filename = null; - $scope.configFormats = [ - "Keycloak JSON", - "Wildfly/EAP Subsystem XML" - ]; + var protocol = client.protocol; + if (!protocol) protocol = 'openid-connect'; + $scope.configFormats = serverInfo.clientInstallations[protocol]; + console.log('configFormats.length: ' + $scope.configFormats.length); + $scope.changeFormat = function() { + var url = ClientInstallation.url({ realm: $routeParams.realm, client: $routeParams.client, provider: $scope.configFormat.id }); + $http.get(url).success(function(data) { + var installation = data; + if ($scope.configFormat.mediaType == 'application/json') { + installation = angular.fromJson(data); + installation = angular.toJson(installation, true); + } + $scope.installation = installation; + }) + }; + + + /* $scope.changeFormat = function() { if ($scope.configFormat == "Keycloak JSON") { $scope.filename = 'keycloak.json'; @@ -720,9 +734,10 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, Clie console.debug($scope.filename); }; + */ $scope.download = function() { - saveAs(new Blob([$scope.installation], { type: $scope.type }), $scope.filename); + saveAs(new Blob([$scope.installation], { type: $scope.configFormat.mediaType }), $scope.configFormat.filename); } }); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 94854f30e6..e949a9d5e5 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -284,15 +284,6 @@ module.factory('ClientClaimsLoader', function(Loader, ClientClaims, $route, $q) }); }); -module.factory('ClientInstallationLoader', function(Loader, ClientInstallation, $route, $q) { - return Loader.get(ClientInstallation, function() { - return { - realm : $route.current.params.realm, - client : $route.current.params.client - } - }); -}); - module.factory('ClientRoleListLoader', function(Loader, ClientRole, $route, $q) { return Loader.query(ClientRole, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 728ed42fa3..5e817442fc 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -1044,16 +1044,28 @@ module.factory('ClientDescriptionConverter', function($resource) { }); }); +/* +module.factory('ClientInstallation', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/installation/providers/:provider', { + realm : '@realm', + client : '@client', + provider : '@provider' + }); +}); +*/ + + module.factory('ClientInstallation', function($resource) { - var url = authUrl + '/admin/realms/:realm/clients/:client/installation/json'; + var url = authUrl + '/admin/realms/:realm/clients/:client/installation/providers/:provider'; return { url : function(parameters) { - return url.replace(':realm', parameters.realm).replace(':client', parameters.client); + return url.replace(':realm', parameters.realm).replace(':client', parameters.client).replace(':provider', parameters.provider); } } }); + module.factory('ClientInstallationJBoss', function($resource) { var url = authUrl + '/admin/realms/:realm/clients/:client/installation/jboss'; return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html index 2e3c578151..1530ba87c6 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html @@ -14,7 +14,7 @@
    -
    diff --git a/services/src/main/java/org/keycloak/protocol/ClientInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/ClientInstallationProvider.java new file mode 100755 index 0000000000..25e8f14d95 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/ClientInstallationProvider.java @@ -0,0 +1,27 @@ +package org.keycloak.protocol; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; + +import javax.ws.rs.core.Response; +import java.net.URI; + +/** + * Provides a template/sample client config adapter file. For example keycloak.json for our OIDC adapter. keycloak-saml.xml for our SAML client adapter + * + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ClientInstallationProvider extends Provider, ProviderFactory { + Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri); + String getProtocol(); + String getDisplayType(); + String getHelpText(); + String getFilename(); + String getMediaType(); + boolean isDownloadOnly(); +} diff --git a/services/src/main/java/org/keycloak/protocol/ClientInstallationSpi.java b/services/src/main/java/org/keycloak/protocol/ClientInstallationSpi.java new file mode 100755 index 0000000000..fa1b54e63a --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/ClientInstallationSpi.java @@ -0,0 +1,32 @@ +package org.keycloak.protocol; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class ClientInstallationSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "client-installation"; + } + + @Override + public Class getProviderClass() { + return ClientInstallationProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ClientInstallationProvider.class; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java new file mode 100755 index 0000000000..bd87517479 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java @@ -0,0 +1,129 @@ +package org.keycloak.protocol.oidc.installation; + +import org.keycloak.Config; +import org.keycloak.authentication.ClientAuthenticator; +import org.keycloak.authentication.ClientAuthenticatorFactory; +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.oidc.OIDCLoginProtocol; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakOIDCClientInstallation implements ClientInstallationProvider { + + @Override + public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) { + ClientManager.InstallationAdapterConfig rep = new ClientManager.InstallationAdapterConfig(); + rep.setAuthServerUrl(baseUri.toString()); + rep.setRealm(realm.getName()); + rep.setRealmKey(realm.getPublicKeyPem()); + rep.setSslRequired(realm.getSslRequired().name().toLowerCase()); + + if (client.isPublicClient() && !client.isBearerOnly()) rep.setPublicClient(true); + if (client.isBearerOnly()) rep.setBearerOnly(true); + if (client.getRoles().size() > 0) rep.setUseResourceRoleMappings(true); + + rep.setResource(client.getClientId()); + + if (showClientCredentialsAdapterConfig(client)) { + Map adapterConfig = getClientCredentialsAdapterConfig(session, client); + rep.setCredentials(adapterConfig); + } + String json = null; + try { + json = JsonSerialization.writeValueAsPrettyString(rep); + } catch (IOException e) { + throw new RuntimeException(e); + } + return Response.ok(json, MediaType.TEXT_PLAIN_TYPE).build(); + } + + public static Map getClientCredentialsAdapterConfig(KeycloakSession session, ClientModel client) { + String clientAuthenticator = client.getClientAuthenticatorType(); + ClientAuthenticatorFactory authenticator = (ClientAuthenticatorFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, clientAuthenticator); + return authenticator.getAdapterConfiguration(client); + } + + + public static boolean showClientCredentialsAdapterConfig(ClientModel client) { + if (client.isPublicClient()) { + return false; + } + + if (client.isBearerOnly() && client.getNodeReRegistrationTimeout() <= 0) { + return false; + } + + return true; + } + + + @Override + public String getProtocol() { + return OIDCLoginProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Keycloak OIDC keycloak.json"; + } + + @Override + public String getHelpText() { + return "keycloak.json file used by the Keycloak OIDC client adapter to configure clients. This must be saved to a keycloak.json file and put in your WEB-INF directory of your WAR file. You may also want to tweak this file after you download it."; + } + + @Override + public void close() { + + } + + @Override + public ClientInstallationProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "keycloak-oidc-keycloak-json"; + } + + @Override + public boolean isDownloadOnly() { + return false; + } + + @Override + public String getFilename() { + return "keycloak.json"; + } + + @Override + public String getMediaType() { + return MediaType.APPLICATION_JSON; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java new file mode 100755 index 0000000000..87514414fe --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java @@ -0,0 +1,117 @@ +package org.keycloak.protocol.oidc.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.oidc.OIDCLoginProtocol; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakOIDCJbossSubsystemClientInstallation implements ClientInstallationProvider { + @Override + public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) { + StringBuffer buffer = new StringBuffer(); + buffer.append("\n"); + buffer.append(" ").append(realm.getName()).append("\n"); + buffer.append(" ").append(realm.getPublicKeyPem()).append("\n"); + buffer.append(" ").append(baseUri.toString()).append("\n"); + if (client.isBearerOnly()){ + buffer.append(" true\n"); + + } else if (client.isPublicClient()) { + buffer.append(" true\n"); + } + buffer.append(" ").append(realm.getSslRequired().name()).append("\n"); + buffer.append(" ").append(client.getClientId()).append("\n"); + String cred = client.getSecret(); + if (KeycloakOIDCClientInstallation.showClientCredentialsAdapterConfig(client)) { + Map adapterConfig = KeycloakOIDCClientInstallation.getClientCredentialsAdapterConfig(session, client); + for (Map.Entry entry : adapterConfig.entrySet()) { + buffer.append(" "); + + Object value = entry.getValue(); + if (value instanceof Map) { + buffer.append("\n"); + Map asMap = (Map) value; + for (Map.Entry credEntry : asMap.entrySet()) { + buffer.append(" <" + credEntry.getKey() + ">" + credEntry.getValue().toString() + "\n"); + } + buffer.append(" \n"); + } else { + buffer.append(value.toString()).append("\n"); + } + } + } + if (client.getRoles().size() > 0) { + buffer.append(" true\n"); + } + buffer.append("\n"); + return Response.ok(buffer.toString(), MediaType.TEXT_PLAIN_TYPE).build(); + } + + @Override + public String getProtocol() { + return OIDCLoginProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Keycloak OIDC JBoss Subsystem XML"; + } + + @Override + public String getHelpText() { + return "XML snippet you must edit and add to the Keycloak OIDC subsystem on your client app server. This type of configuration is useful when you can't or don't want to crack open your WAR file."; + } + + @Override + public void close() { + + } + + @Override + public ClientInstallationProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "keycloak-oidc-jboss-subsystem"; + } + + @Override + public boolean isDownloadOnly() { + return false; + } + + @Override + public String getFilename() { + return "keycloak-oidc-subsystem.xml"; + } + + @Override + public String getMediaType() { + return MediaType.APPLICATION_XML; + } +} + 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 03b06360e8..6cbfed03fe 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 @@ -17,6 +17,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.ClientInstallationProvider; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -143,6 +144,15 @@ public class ClientResource { return new ClientAttributeCertificateResource(realm, auth, client, session, attributePrefix, adminEvent); } + @GET + @NoCache + @Path("installation/providers/{providerId}") + public Response getInstallationProvider(@PathParam("providerId") String providerId) { + ClientInstallationProvider provider = session.getProvider(ClientInstallationProvider.class, providerId); + if (provider == null) throw new NotFoundException("Unknown Provider"); + return provider.generateInstallation(session, realm, client, keycloak.getBaseUri(uriInfo)); + } + /** * Get keycloak.json file 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 672745cb66..dfefb10792 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 @@ -21,6 +21,7 @@ import org.keycloak.freemarker.ThemeProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.protocol.ClientInstallationProvider; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.ProtocolMapper; @@ -61,6 +62,7 @@ public class ServerInfoAdminResource { setProviders(info); setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); + setClientInstallations(info); info.setEnums(ENUMS); return info; } @@ -144,6 +146,27 @@ public class ServerInfoAdminResource { } } + private void setClientInstallations(ServerInfoRepresentation info) { + info.setClientInstallations(new HashMap>()); + for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ClientInstallationProvider.class)) { + ClientInstallationProvider provider = (ClientInstallationProvider)p; + List types = info.getClientInstallations().get(provider.getProtocol()); + if (types == null) { + types = new LinkedList<>(); + info.getClientInstallations().put(provider.getProtocol(), types); + } + ClientInstallationRepresentation rep = new ClientInstallationRepresentation(); + rep.setId(p.getId()); + rep.setHelpText(provider.getHelpText()); + rep.setDisplayType( provider.getDisplayType()); + rep.setProtocol( provider.getProtocol()); + rep.setDownloadOnly( provider.isDownloadOnly()); + rep.setFilename(provider.getFilename()); + rep.setMediaType(provider.getMediaType()); + types.add(rep); + } + } + private void setProtocolMapperTypes(ServerInfoRepresentation info) { info.setProtocolMapperTypes(new HashMap>()); for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ProtocolMapper.class)) { 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 new file mode 100755 index 0000000000..c3da08688f --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider @@ -0,0 +1,2 @@ +org.keycloak.protocol.oidc.installation.KeycloakOIDCClientInstallation +org.keycloak.protocol.oidc.installation.KeycloakOIDCJbossSubsystemClientInstallation diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 6189178bfe..a657c78891 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -9,3 +9,4 @@ org.keycloak.authentication.RequiredActionSpi org.keycloak.authentication.FormAuthenticatorSpi org.keycloak.authentication.FormActionSpi org.keycloak.services.clientregistration.ClientRegistrationSpi +org.keycloak.protocol.ClientInstallationSpi diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java index 4f0936be4a..a6f8a44e17 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -122,6 +122,7 @@ public class RealmTest extends AbstractClientTest { realm2 = keycloak.realms().realm("test-immutable").toRepresentation(); keycloak.realms().realm("test-immutable-old").remove(); + keycloak.realms().realm("test-immutable").remove(); From 4a4bbf26f465fa364c6a127e539e998a22dfe7a9 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 6 Jan 2016 16:51:02 -0500 Subject: [PATCH 60/65] installation provider --- .../keycloak/services/resources/admin/ClientResource.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 6cbfed03fe..eef260b191 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 @@ -157,11 +157,14 @@ public class ClientResource { /** * Get keycloak.json file * + * this method is deprecated, see getInstallationProvider + * * Returns a keycloak.json file to be used to configure the adapter of the specified client. * * @return * @throws IOException */ + @Deprecated @GET @NoCache @Path("installation/json") @@ -179,11 +182,14 @@ public class ClientResource { /** * Get adapter configuration XML for JBoss / Wildfly Keycloak subsystem * + * this method is deprecated, see getInstallationProvider + * * Returns XML that can be included in the JBoss / Wildfly Keycloak subsystem to configure the adapter of that client. * * @return * @throws IOException */ + @Deprecated @GET @NoCache @Path("installation/jboss") From 54712e29aa8b86aa883424ed9a3e22dfc507d381 Mon Sep 17 00:00:00 2001 From: mhajas Date: Tue, 22 Dec 2015 13:04:39 +0100 Subject: [PATCH 61/65] Use user script for admin user creating --- .../testsuite/AbstractKeycloakTest.java | 6 -- .../src/test/resources/keycloak-add-user.json | 16 ++++ .../integration-arquillian/tests/pom.xml | 76 +++++++++++++++++++ 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/keycloak-add-user.json 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 13ec96e4d6..e74a646d6c 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 @@ -90,12 +90,6 @@ public abstract class AbstractKeycloakTest { driverSettings(); - if (!suiteContext.isAdminPasswordUpdated()) { - log.debug("updating admin password"); - updateMasterAdminPassword(); - suiteContext.setAdminPasswordUpdated(true); - } - importTestRealms(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/keycloak-add-user.json b/testsuite/integration-arquillian/tests/base/src/test/resources/keycloak-add-user.json new file mode 100644 index 0000000000..635f144a8e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/keycloak-add-user.json @@ -0,0 +1,16 @@ +[ { + "realm" : "master", + "users" : [ { + "username" : "admin", + "enabled" : true, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "6K5rvcPu3dXndZOhpzLAVbFtcdlUhbGCrUyV0NNzeS61IdhMpjH8Mf4y/Ag/vHZkw4Ayvtvb9/1iMNOzxR0M6g==", + "salt" : "/6M1jTMUB0uR8EOkksFn/A==", + "hashIterations" : 100000, + "algorithm" : "pbkdf2" + } ], + "realmRoles" : [ "admin" ] + } ], + "identityFederationEnabled" : false +} ] \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index bfac920ae3..a4c09e6100 100644 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -91,6 +91,7 @@ ${auth.server.management.port.jmx} ${auth.server.ssl.required} ${startup.timeout.sec} + ${project.build.directory}/undertow-configuration @@ -127,6 +128,31 @@ + + maven-resources-plugin + 2.7 + + + copy-admin-user-json-file + process-resources + + copy-resources + + + ${project.build.directory}/undertow-configuration + + + src/test/resources + + keycloak-add-user.json + + true + + + + + + @@ -413,6 +439,31 @@ + + maven-resources-plugin + 2.7 + + + copy-admin-user-json-file + process-resources + + copy-resources + + + ${auth.server.wildfly.home}/standalone/configuration + + + src/test/resources + + keycloak-add-user.json + + true + + + + + + @@ -473,6 +524,31 @@ + + maven-resources-plugin + 2.7 + + + copy-admin-user-json-file + process-resources + + copy-resources + + + ${auth.server.eap6.home}/standalone/configuration + + + src/test/resources + + keycloak-add-user.json + + true + + + + + + From 65f5ce9f1340e287297b78080876db09a5e98368 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 7 Jan 2016 13:09:53 +0100 Subject: [PATCH 62/65] KEYCLOAK-2252 Cannot delete client mapper with delete icon --- .../theme/base/admin/resources/js/controllers/clients.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 14243077af..b2d77ffcd1 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1627,7 +1627,7 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo }; $scope.remove = function() { - Dialog.confirmDelete($scope.mapper.name, 'mapper', function() { + Dialog.confirmDelete($scope.model.mapper.name, 'mapper', function() { ClientProtocolMapper.remove({ realm: realm.realm, client: client.id, id : $scope.model.mapper.id }, function() { Notifications.success("The mapper has been deleted."); $location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers"); From 8695e169719929462855e63ae3707f0886555488 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 7 Jan 2016 13:16:40 +0100 Subject: [PATCH 63/65] KEYCLOAK-2269 add-user script adds identityFederationEnabled field to keycloak-add-user.json --- .../keycloak/representations/idm/RealmRepresentation.java | 5 ----- 1 file changed, 5 deletions(-) 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 3359f2195f..ed5fd9351e 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -642,11 +642,6 @@ public class RealmRepresentation { identityProviders.add(identityProviderRepresentation); } - @Deprecated - public boolean isIdentityFederationEnabled() { - return identityProviders != null && !identityProviders.isEmpty(); - } - public List getProtocolMappers() { return protocolMappers; } From 4b1b697aa352f40f6088aa94cb4701002d675ab3 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 7 Jan 2016 16:19:59 +0100 Subject: [PATCH 64/65] KEYCLOAK-2127 Re-enable set admin user with welcome page --- .../java/org/keycloak/testsuite/AbstractKeycloakTest.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 e74a646d6c..13ec96e4d6 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 @@ -90,6 +90,12 @@ public abstract class AbstractKeycloakTest { driverSettings(); + if (!suiteContext.isAdminPasswordUpdated()) { + log.debug("updating admin password"); + updateMasterAdminPassword(); + suiteContext.setAdminPasswordUpdated(true); + } + importTestRealms(); } From 78fe064cf0a942488961f6d7b2c17f3237c2bb2c Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 7 Jan 2016 17:25:47 -0500 Subject: [PATCH 65/65] 2213 --- .../admin/resources/js/controllers/clients.js | 29 +--- .../resources/templates/kc-tabs-client.html | 2 +- model/sessions-infinispan/pom.xml | 12 ++ .../config/parsers/DeploymentBuilder.java | 26 ++- .../EntityDescriptorDescriptionConverter.java | 2 +- .../keycloak/protocol/saml/SamlClient.java | 17 ++ .../protocol/saml/SamlConfigAttributes.java | 2 + .../keycloak/protocol/saml/SamlProtocol.java | 1 - .../protocol/saml/SamlProtocolUtils.java | 2 +- .../KeycloakSamlClientInstallation.java | 161 ++++++++++++++++++ ...ycloak.protocol.ClientInstallationProvider | 1 + .../services/resources/RealmsResource.java | 4 + 12 files changed, 221 insertions(+), 38 deletions(-) create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java create mode 100755 saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index f80e57a3dd..9729c9cd90 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -709,33 +709,6 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv $scope.installation = installation; }) }; - - - /* - $scope.changeFormat = function() { - if ($scope.configFormat == "Keycloak JSON") { - $scope.filename = 'keycloak.json'; - - var url = ClientInstallation.url({ realm: $routeParams.realm, client: $routeParams.client }); - $http.get(url).success(function(data) { - var tmp = angular.fromJson(data); - $scope.installation = angular.toJson(tmp, true); - $scope.type = 'application/json'; - }) - } else if ($scope.configFormat == "Wildfly/EAP Subsystem XML") { - $scope.filename = 'keycloak.xml'; - - var url = ClientInstallationJBoss.url({ realm: $routeParams.realm, client: $routeParams.client }); - $http.get(url).success(function(data) { - $scope.installation = data; - $scope.type = 'text/xml'; - }) - } - - console.debug($scope.filename); - }; - */ - $scope.download = function() { saveAs(new Blob([$scope.installation], { type: $scope.configFormat.mediaType }), $scope.configFormat.filename); } @@ -1080,7 +1053,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 = ['openid-connect', 'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort(); - + $scope.create = true; $scope.templates = [ {name:'NONE'}]; for (var i = 0; i < templates.length; i++) { var template = templates[i]; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html index 6f564673b2..58388ff64b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html @@ -33,7 +33,7 @@
  • {{:: 'clustering' | translate}}
  • -
  • +
  • {{:: 'installation' | translate}} {{:: 'installation.tooltip' | translate}}
  • diff --git a/model/sessions-infinispan/pom.xml b/model/sessions-infinispan/pom.xml index c4f57b0333..553895cf81 100755 --- a/model/sessions-infinispan/pom.xml +++ b/model/sessions-infinispan/pom.xml @@ -36,4 +36,16 @@ test + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java index 5de2072350..e0564e926f 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java @@ -98,12 +98,26 @@ public class DeploymentBuilder { } if (key.isEncryption()) { - KeyStore keyStore = loadKeystore(resourceLoader, key); - try { - PrivateKey privateKey = (PrivateKey) keyStore.getKey(key.getKeystore().getPrivateKeyAlias(), key.getKeystore().getPrivateKeyPassword().toCharArray()); - deployment.setDecryptionKey(privateKey); - } catch (Exception e) { - throw new RuntimeException(e); + if (key.getKeystore() != null) { + + KeyStore keyStore = loadKeystore(resourceLoader, key); + try { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(key.getKeystore().getPrivateKeyAlias(), key.getKeystore().getPrivateKeyPassword().toCharArray()); + deployment.setDecryptionKey(privateKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + if (key.getPrivateKeyPem() == null) { + throw new RuntimeException("SP signing key must have a PrivateKey defined"); + } + try { + PrivateKey privateKey = PemUtils.decodePrivateKey(key.getPrivateKeyPem().trim()); + deployment.setDecryptionKey(privateKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + } } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java index 20f78109f4..61ecbdcdc3 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java @@ -116,7 +116,7 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo attributes.put(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem); } else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) { attributes.put(SamlConfigAttributes.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); - attributes.put(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem); + attributes.put(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem); } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java index d935f83e2a..3ac9892d2e 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java @@ -121,4 +121,21 @@ public class SamlClient extends ClientConfigResolver { } + public String getClientEncryptingCertificate() { + return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE); + } + + public void setClientEncryptingCertificate(String val) { + client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, val); + + } + public String getClientEncryptingPrivateKey() { + return client.getAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE); + } + + public void setClientEncryptingPrivateKey(String val) { + client.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE, val); + + } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java index eea258ac0b..c6bc60a607 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java @@ -19,4 +19,6 @@ public interface SamlConfigAttributes { String SAML_ENCRYPT = "saml.encrypt"; String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature"; String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE; + String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE; + String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.PRIVATE_KEY; } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index e7bd3ebd4d..c76c853d74 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -70,7 +70,6 @@ public class SamlProtocol implements LoginProtocol { public static final String ATTRIBUTE_TRUE_VALUE = "true"; public static final String ATTRIBUTE_FALSE_VALUE = "false"; - public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE; public static final String SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE = "saml_assertion_consumer_url_post"; public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE = "saml_assertion_consumer_url_redirect"; public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post"; diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java index 5742f7d1a3..3e03ed4201 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java @@ -49,7 +49,7 @@ public class SamlProtocolUtils { } public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException { - return getPublicKey(client, SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE); + return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE); } public static PublicKey getPublicKey(ClientModel client, String attribute) throws VerificationException { diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java new file mode 100755 index 0000000000..d3abddf054 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java @@ -0,0 +1,161 @@ +package org.keycloak.protocol.saml.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.saml.SamlClient; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.services.resources.RealmsResource; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakSamlClientInstallation implements ClientInstallationProvider { + + @Override + public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) { + SamlClient samlClient = new SamlClient(client); + StringBuffer buffer = new StringBuffer(); + buffer.append("\n"); + buffer.append(" \n"); + if (samlClient.requiresClientSignature() || samlClient.requiresEncryption()) { + buffer.append(" \n"); + if (samlClient.requiresClientSignature()) { + buffer.append(" \n"); + buffer.append(" \n"); + if (samlClient.getClientSigningPrivateKey() == null) { + buffer.append(" PRIVATE KEY NOT SET UP OR KNOWN\n"); + } else { + buffer.append(" ").append(samlClient.getClientSigningPrivateKey()).append("\n"); + } + buffer.append(" \n"); + buffer.append(" \n"); + if (samlClient.getClientSigningCertificate() == null) { + buffer.append(" YOU MUST CONFIGURE YOUR CLIENT's SIGNING CERTIFICATE\n"); + } else { + buffer.append(" ").append(samlClient.getClientSigningCertificate()).append("\n"); + } + buffer.append(" \n"); + buffer.append(" \n"); + } + if (samlClient.requiresEncryption()) { + buffer.append(" \n"); + buffer.append(" \n"); + if (samlClient.getClientEncryptingPrivateKey() == null) { + buffer.append(" PRIVATE KEY NOT SET UP OR KNOWN\n"); + } else { + buffer.append(" ").append(samlClient.getClientEncryptingPrivateKey()).append("\n"); + } + buffer.append(" \n"); + buffer.append(" \n"); + + } + buffer.append(" \n"); + } + buffer.append(" \n"); + buffer.append(" \n"); + + buffer.append(" \n"); + if (samlClient.requiresRealmSignature()) { + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" ").append(realm.getCertificatePem()).append("\n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" \n"); + } + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append("\n"); + return Response.ok(buffer.toString(), MediaType.TEXT_PLAIN_TYPE).build(); + } + + @Override + public String getProtocol() { + return SamlProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Keycloak SAML Adapter keycloak-saml.xml"; + } + + @Override + public String getHelpText() { + return "Keycloak SAML adapter configuration file. Put this in WEB-INF directory if your WAR."; + } + + @Override + public String getFilename() { + return "keycloak-saml.xml"; + } + + @Override + public String getMediaType() { + return MediaType.APPLICATION_XML; + } + + @Override + public boolean isDownloadOnly() { + return false; + } + + @Override + public void close() { + + } + + @Override + public ClientInstallationProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "keycloak-saml"; + } +} diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider new file mode 100755 index 0000000000..f8e9df55e2 --- /dev/null +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider @@ -0,0 +1 @@ +org.keycloak.protocol.saml.installation.KeycloakSamlClientInstallation 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 a1a3accb9d..e0ea2003e7 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -57,6 +57,10 @@ public class RealmsResource { return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getProtocol"); } + public static UriBuilder protocolUrl(UriBuilder builder) { + return builder.path(RealmsResource.class).path(RealmsResource.class, "getProtocol"); + } + public static UriBuilder clientRegistrationUrl(UriInfo uriInfo) { return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getClientsService"); }