This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/master by this push: new e712d63 CAMEL-14977: Create camel-undertow-spring-security component (#3782) e712d63 is described below commit e712d63478a8ff456a3543596cf43f8cf4107262 Author: JiriOndrusek <ondrusek.j...@gmail.com> AuthorDate: Tue Apr 28 18:59:20 2020 +0200 CAMEL-14977: Create camel-undertow-spring-security component (#3782) --- apache-camel/src/main/descriptors/common-bin.xml | 1 + components/camel-undertow-spring-security/pom.xml | 88 +++++++++++++++ .../docs/undertow-spring-security-component.adoc | 32 ++++++ .../security/SpringSecurityConfiguration.java | 29 +++++ .../spring/security/SpringSecurityProvider.java | 100 +++++++++++++++++ .../KeycloakJwtAuthenticationConverter.java | 29 +++++ .../keycloak/KeycloakRealmRoleConverter.java | 44 ++++++++ .../keycloak/KeycloakUsernameSubClaimAdapter.java | 46 ++++++++ ...component.undertow.spi.UndertowSecurityProvider | 1 + .../AbstractSpringSecurityBearerTokenTest.java | 122 +++++++++++++++++++++ .../component/spring/security/MockFilter.java | 68 ++++++++++++ .../security/SpringSecurityBearerTokenTest.java | 66 +++++++++++ .../src/test/resources/log4j2.properties | 29 +++++ .../src/main/docs/undertow-component.adoc | 4 + .../component/undertow/DefaultUndertowHost.java | 11 +- components/pom.xml | 1 + docs/components/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/undertow-component.adoc | 4 + .../pages/undertow-spring-security-component.adoc | 34 ++++++ parent/pom.xml | 5 + 20 files changed, 712 insertions(+), 3 deletions(-) diff --git a/apache-camel/src/main/descriptors/common-bin.xml b/apache-camel/src/main/descriptors/common-bin.xml index c2efcc3..823fda0 100644 --- a/apache-camel/src/main/descriptors/common-bin.xml +++ b/apache-camel/src/main/descriptors/common-bin.xml @@ -366,6 +366,7 @@ <include>org.apache.camel:camel-twilio</include> <include>org.apache.camel:camel-twitter</include> <include>org.apache.camel:camel-undertow</include> + <include>org.apache.camel:camel-undertow-spring-security</include> <include>org.apache.camel:camel-univocity-parsers</include> <include>org.apache.camel:camel-validator</include> <include>org.apache.camel:camel-velocity</include> diff --git a/components/camel-undertow-spring-security/pom.xml b/components/camel-undertow-spring-security/pom.xml new file mode 100644 index 0000000..23a9010 --- /dev/null +++ b/components/camel-undertow-spring-security/pom.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.camel</groupId> + <artifactId>components</artifactId> + <version>3.3.0-SNAPSHOT</version> + </parent> + + <artifactId>camel-undertow-spring-security</artifactId> + <packaging>jar</packaging> + + <name>Camel :: Undertow Spring Security</name> + <description>Spring Security Provider for camel-undertow</description> + + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-undertow</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-oauth2-client</artifactId> + <version>${spring-security-version}</version> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-oauth2-jose</artifactId> + <version>${spring-security-version}</version> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-oauth2-resource-server</artifactId> + <version>${spring-security-version}</version> + </dependency> + + <!-- testing --> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.nimbusds</groupId> + <artifactId>nimbus-jose-jwt</artifactId> + <version>${nimbus-jose-jwt}</version> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.camel</groupId> + <artifactId>camel-package-maven-plugin</artifactId> + <executions> + <execution> + <id>generate</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + + +</project> diff --git a/components/camel-undertow-spring-security/src/main/docs/undertow-spring-security-component.adoc b/components/camel-undertow-spring-security/src/main/docs/undertow-spring-security-component.adoc new file mode 100644 index 0000000..e2f8318 --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/docs/undertow-spring-security-component.adoc @@ -0,0 +1,32 @@ +[[undertow-spring-security-component]] += Undertow Spring Security Security Provider +//by hand +:since: 3.3 + +*Since Camel {since}* + +*OSGi is not supported* + + +The Spring Security Provider provides Spring Security (5.x) token bearer security over camel-undertow component. +To force camel-undertow to use spring security provider: + +* Add spring security provider library on classpath. +* Provide instance of SpringSecurityConfiguration as `securityConfiguration` +parameter into camel-undertow component or provide both `securityConfiguration` and `securityProvider` +into camel-undertow component. +* Configure spring-security. + +Configuration has to provide following security attribute: +[width="100%"] +|=== +| Name | Description | Type +| *securityFiler* | Provides security filter gained from configured spring security (5.x). Filter could be obtained +for example from DelegatingFilterProxyRegistrationBean. | Filter +|=== + +Each exchange created by Undertow endpoint with spring security contains header 'SpringSecurityProvider_principal' ( +name of header is provided as a constant `SpringSecurityProvider.PRINCIPAL_NAME_HEADER`) with current authorized identity +as value or header is not present in case of rejected requests. + + diff --git a/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/SpringSecurityConfiguration.java b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/SpringSecurityConfiguration.java new file mode 100644 index 0000000..72cc891 --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/SpringSecurityConfiguration.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security; + +import javax.servlet.Filter; + +public interface SpringSecurityConfiguration { + + /** + * Provides security filter gained from configured spring security (5+). + * Filter could be obtained for example from DelegatingFilterProxyRegistrationBean. + */ + Filter getSecurityFilter(); + +} diff --git a/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/SpringSecurityProvider.java b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/SpringSecurityProvider.java new file mode 100644 index 0000000..09f46f9 --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/SpringSecurityProvider.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security; + +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import io.undertow.server.HttpServerExchange; +import io.undertow.servlet.handlers.ServletRequestContext; +import io.undertow.util.AttachmentKey; +import io.undertow.util.StatusCodes; +import org.apache.camel.component.undertow.spi.UndertowSecurityProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +public class SpringSecurityProvider implements UndertowSecurityProvider { + + public static final String PRINCIPAL_NAME_HEADER = SpringSecurityProvider.class.getName() + "_principal"; + private static final AttachmentKey<String> PRINCIPAL_NAME_KEY = AttachmentKey.create(String.class); + + private Filter securityFilter; + + @Override + public void addHeader(BiConsumer<String, Object> consumer, HttpServerExchange httpExchange) throws Exception { + String principalName = httpExchange.getAttachment(PRINCIPAL_NAME_KEY); + consumer.accept(PRINCIPAL_NAME_HEADER, principalName); + } + + @Override + public int authenticate(HttpServerExchange httpExchange, List<String> allowedRoles) throws Exception { + ServletRequestContext servletRequestContext = httpExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); + ServletRequest request = servletRequestContext.getServletRequest(); + ServletResponse response = servletRequestContext.getServletResponse(); + + //new filter has to be added into the filter chain. If is successfully called it means that security allows access. + FilterChain fc = (servletRequest, servletResponse) -> { + Authentication a = SecurityContextHolder.getContext().getAuthentication(); + if (a instanceof JwtAuthenticationToken) { + boolean allowed = false; + Collection<GrantedAuthority> grantedAuthorities = ((JwtAuthenticationToken) a).getAuthorities(); + for (GrantedAuthority grantedAuthority : grantedAuthorities) { + if (allowedRoles.contains(grantedAuthority.getAuthority())) { + allowed = true; + break; + } + } + + if (allowed) { + httpExchange.putAttachment(PRINCIPAL_NAME_KEY, ((JwtAuthenticationToken) a).getName()); + httpExchange.setStatusCode(StatusCodes.OK); + return; + } + + httpExchange.setStatusCode(StatusCodes.FORBIDDEN); + } + }; + securityFilter.doFilter(request, response, fc); + + return httpExchange.getStatusCode(); + } + + + @Override + public boolean acceptConfiguration(Object configuration, String endpointUri) throws Exception { + if (configuration instanceof SpringSecurityConfiguration) { + SpringSecurityConfiguration conf = (SpringSecurityConfiguration) configuration; + this.securityFilter = conf.getSecurityFilter(); + return true; + } + + return false; + } + + @Override + public boolean requireServletContext() { + return true; + } +} diff --git a/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakJwtAuthenticationConverter.java b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakJwtAuthenticationConverter.java new file mode 100644 index 0000000..b2823aa --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakJwtAuthenticationConverter.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security.keycloak; + +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; + +/** + * JwtAuthentication converter prepared with KeycloakRealmRoleConverter. + */ +public class KeycloakJwtAuthenticationConverter extends JwtAuthenticationConverter { + + public KeycloakJwtAuthenticationConverter() { + setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter()); + } +} diff --git a/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakRealmRoleConverter.java b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakRealmRoleConverter.java new file mode 100644 index 0000000..8dfd2be --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakRealmRoleConverter.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security.keycloak; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * Converts JWT token into list of roles. + */ +public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> { + + public static final String REALM_ACCESS = "realm_access"; + public static final String ROLES = "roles"; + + @Override + public Collection<GrantedAuthority> convert(final Jwt jwt) { + final Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get(REALM_ACCESS); + return ((List<String>)realmAccess.get(ROLES)).stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} diff --git a/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakUsernameSubClaimAdapter.java b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakUsernameSubClaimAdapter.java new file mode 100644 index 0000000..05a284d --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/java/org/apache/camel/component/spring/security/keycloak/KeycloakUsernameSubClaimAdapter.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security.keycloak; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter; + +/** + * See https://docs.spring.io/spring-security/site/docs/5.2.x/reference/html5/#oauth2resourceserver-jwt-claimsetmapping-rename + * for more information. + */ +public class KeycloakUsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> { + + private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); + + private final String userNameAttribute; + + public KeycloakUsernameSubClaimAdapter(String userNameAttribute) { + this.userNameAttribute = userNameAttribute; + } + + @Override + public Map<String, Object> convert(Map<String, Object> claims) { + Map<String, Object> convertedClaims = this.delegate.convert(claims); + String username = (String) convertedClaims.get(userNameAttribute); + convertedClaims.put("sub", username); + return convertedClaims; + } +} \ No newline at end of file diff --git a/components/camel-undertow-spring-security/src/main/resources/META-INF/services/org.apache.camel.component.undertow.spi.UndertowSecurityProvider b/components/camel-undertow-spring-security/src/main/resources/META-INF/services/org.apache.camel.component.undertow.spi.UndertowSecurityProvider new file mode 100644 index 0000000..d185902 --- /dev/null +++ b/components/camel-undertow-spring-security/src/main/resources/META-INF/services/org.apache.camel.component.undertow.spi.UndertowSecurityProvider @@ -0,0 +1 @@ +org.apache.camel.component.spring.security.SpringSecurityProvider \ No newline at end of file diff --git a/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/AbstractSpringSecurityBearerTokenTest.java b/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/AbstractSpringSecurityBearerTokenTest.java new file mode 100644 index 0000000..c233852 --- /dev/null +++ b/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/AbstractSpringSecurityBearerTokenTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security; + +import java.io.File; +import java.io.FileWriter; +import java.io.Writer; +import java.net.URL; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.Filter; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import org.apache.camel.BindToRegistry; +import org.apache.camel.CamelContext; +import org.apache.camel.component.spring.security.keycloak.KeycloakRealmRoleConverter; +import org.apache.camel.component.spring.security.keycloak.KeycloakUsernameSubClaimAdapter; +import org.apache.camel.component.undertow.UndertowComponent; +import org.apache.camel.component.undertow.spi.UndertowSecurityProvider; +import org.apache.camel.test.AvailablePortFinder; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.BeforeClass; +import org.springframework.security.oauth2.jwt.Jwt; + +public abstract class AbstractSpringSecurityBearerTokenTest extends CamelTestSupport { + + private static volatile int port; + + private final MockFilter mockFilter = new MockFilter(); + + public MockFilter getMockFilter() { + return mockFilter; + } + + @BeforeClass + public static void initPort() throws Exception { + port = AvailablePortFinder.getNextAvailable(); + + URL location = SpringSecurityProvider.class.getProtectionDomain().getCodeSource().getLocation(); + File file = new File(location.getPath() + "META-INF/services/" + UndertowSecurityProvider.class.getName()); + file.getParentFile().mkdirs(); + + Writer output = new FileWriter(file); + output.write(SpringSecurityProvider.class.getName()); + output.close(); + + file.deleteOnExit(); + } + + protected static int getPort() { + return port; + } + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + + context.getPropertiesComponent().setLocation("ref:prop"); + + context.getComponent("undertow", UndertowComponent.class).setSecurityConfiguration(new SpringSecurityConfiguration() { + @Override + public Filter getSecurityFilter() { + return mockFilter; + } + }); + + return context; + } + + @BindToRegistry("prop") + public Properties loadProperties() throws Exception { + + Properties prop = new Properties(); + prop.setProperty("port", "" + getPort()); + return prop; + } + + Jwt createToken(String userName, String role) { + JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder(); + + claimsSet.subject("123445667"); + claimsSet.claim("preffered_name", userName); + claimsSet.audience("resource-server"); + claimsSet.issuer("came-spring-security"); + + PlainJWT plainJWT = new PlainJWT(claimsSet.build()); + + Map<String, Object> headers = new HashMap(); + headers.put("type", "JWT"); + headers.put("alg", "RS256"); + Map<String, Object> claims = new KeycloakUsernameSubClaimAdapter("preffered_name").convert(claimsSet.getClaims()); + + JSONArray roles = new JSONArray(); + roles.appendElement(role); + JSONObject r = new JSONObject(); + r.put(KeycloakRealmRoleConverter.ROLES, roles); + claims.put(KeycloakRealmRoleConverter.REALM_ACCESS, new JSONObject(r)); + + Jwt retVal = new Jwt(plainJWT.serialize(), Instant.now(), Instant.now().plusSeconds(10), headers, claims); + return retVal; + } +} diff --git a/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/MockFilter.java b/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/MockFilter.java new file mode 100644 index 0000000..c7ed518 --- /dev/null +++ b/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/MockFilter.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security; + +import java.io.IOException; +import java.util.Collection; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.camel.component.spring.security.keycloak.KeycloakRealmRoleConverter; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +public class MockFilter implements Filter { + + private Jwt jwt; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (jwt == null) { + throw new AccessDeniedException("not allowed"); + } + + Collection<? extends GrantedAuthority> grantedAuthorities = new KeycloakRealmRoleConverter().convert(jwt); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt, grantedAuthorities)); + + if (chain != null) { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + + } + + public void setJwt(Jwt jwt) { + this.jwt = jwt; + } +} diff --git a/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/SpringSecurityBearerTokenTest.java b/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/SpringSecurityBearerTokenTest.java new file mode 100644 index 0000000..6080d84 --- /dev/null +++ b/components/camel-undertow-spring-security/src/test/java/org/apache/camel/component/spring/security/SpringSecurityBearerTokenTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.spring.security; + +import io.undertow.util.StatusCodes; +import org.apache.camel.CamelExecutionException; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.http.base.HttpOperationFailedException; +import org.junit.Assert; +import org.junit.Test; + +public class SpringSecurityBearerTokenTest extends AbstractSpringSecurityBearerTokenTest { + + @Test + public void testBearerTokenAccess() throws Exception { + //configure token in mockFilter + getMockFilter().setJwt(createToken("Alice", "user")); + + String response = template.requestBody("undertow:http://localhost:{{port}}/myapp", + "empty body", + String.class); + assertNotNull(response); + assertEquals("Hello Alice!", response); + } + + @Test + public void testBearerTokenForbidden() throws Exception { + //configure token in mockFilter + getMockFilter().setJwt(createToken("Tom", "wrongUser")); + + try { + template.requestBody("undertow:http://localhost:{{port}}/myapp", + "empty body", + String.class); + Assert.fail("Access is denied"); + } catch (CamelExecutionException e) { + HttpOperationFailedException he = assertIsInstanceOf(HttpOperationFailedException.class, e.getCause()); + assertEquals(StatusCodes.FORBIDDEN, he.getStatusCode()); + } + } + + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + public void configure() { + from("undertow:http://localhost:{{port}}/myapp?allowedRoles=user") + .transform(simple("Hello ${in.header." + SpringSecurityProvider.PRINCIPAL_NAME_HEADER + "}!")); + } + }; + } +} diff --git a/components/camel-undertow-spring-security/src/test/resources/log4j2.properties b/components/camel-undertow-spring-security/src/test/resources/log4j2.properties new file mode 100644 index 0000000..239e564 --- /dev/null +++ b/components/camel-undertow-spring-security/src/test/resources/log4j2.properties @@ -0,0 +1,29 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- + +appender.file.type = File +appender.file.name = file +appender.file.fileName = target/camel-spring-security-test.log +appender.file.layout.type = PatternLayout +appender.file.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m%n +appender.out.type = Console +appender.out.name = out +appender.out.layout.type = PatternLayout +appender.out.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m%n +rootLogger.level = INFO +rootLogger.appenderRef.file.ref = file + diff --git a/components/camel-undertow/src/main/docs/undertow-component.adoc b/components/camel-undertow/src/main/docs/undertow-component.adoc index c78eb97..20db802 100644 --- a/components/camel-undertow/src/main/docs/undertow-component.adoc +++ b/components/camel-undertow/src/main/docs/undertow-component.adoc @@ -224,4 +224,8 @@ Java SPI (Service Provider Interfaces). If there is an object passed to componen as parameter `securityConfiguration` and provider accepts it. Provider will be used for authentication of all requests. +Property `requireServletContext` of security providers forces udertow server to start +with servlet context. There will be no servlet actually handled. This feature is meant only +for use with servlet filters, which needs servlet context for their functionality. + include::camel-spring-boot::page$undertow-starter.adoc[] diff --git a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java index 5defbc9..1638cc8 100644 --- a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java +++ b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHost.java @@ -45,6 +45,7 @@ public class DefaultUndertowHost implements UndertowHost { private final RestRootHandler restHandler; private Undertow undertow; private String hostString; + private DeploymentManager deploymentManager; public DefaultUndertowHost(UndertowHostKey key) { this(key, null); @@ -139,15 +140,16 @@ public class DefaultUndertowHost implements UndertowHost { //httpHandler for servlet is ignored, camel handler is used instead of it .addOuterHandlerChainWrapper(h -> handler); - DeploymentManager manager = Servlets.newContainer().addDeployment(deployment); - manager.deploy(); + deploymentManager = Servlets.newContainer().addDeployment(deployment); + deploymentManager.deploy(); try { - return builder.setHandler(manager.start()).build(); + return builder.setHandler(deploymentManager.start()).build(); } catch (ServletException e) { LOG.warn("Failed to start Undertow server on {}://{}:{}, reason: {}", key.getSslContext() != null ? "https" : "http", key.getHost(), key.getPort(), e.getMessage()); throw new RuntimeException(e); } + } return builder.setHandler(handler).build(); @@ -167,6 +169,9 @@ public class DefaultUndertowHost implements UndertowHost { rootHandler.remove(registrationInfo.getUri().getPath(), registrationInfo.getMethodRestrict(), registrationInfo.isMatchOnUriPrefix()); stop = rootHandler.isEmpty(); } + if (deploymentManager != null) { + deploymentManager.undeploy(); + } if (stop) { LOG.info("Stopping Undertow server on {}://{}:{}", key.getSslContext() != null ? "https" : "http", key.getHost(), key.getPort()); diff --git a/components/pom.xml b/components/pom.xml index b4e4923a..bfe1b68 100644 --- a/components/pom.xml +++ b/components/pom.xml @@ -355,6 +355,7 @@ <module>camel-tika</module> <module>camel-twilio</module> <module>camel-twitter</module> + <module>camel-undertow-spring-security</module> <module>camel-univocity-parsers</module> <module>camel-velocity</module> <module>camel-vertx</module> diff --git a/docs/components/modules/ROOT/nav.adoc b/docs/components/modules/ROOT/nav.adoc index 99799e9..b67dbf9 100644 --- a/docs/components/modules/ROOT/nav.adoc +++ b/docs/components/modules/ROOT/nav.adoc @@ -320,6 +320,7 @@ ** xref:twitter-timeline-component.adoc[Twitter Timeline] ** xref:undertow-component.adoc[Undertow] ** xref:elytron-component.adoc[Undertow Elytron Security Provider] +** xref:undertow-spring-security-component.adoc[Undertow Spring Security Security Provider] ** xref:validator-component.adoc[Validator] ** xref:velocity-component.adoc[Velocity] ** xref:vertx-component.adoc[Vert.x] diff --git a/docs/components/modules/ROOT/pages/undertow-component.adoc b/docs/components/modules/ROOT/pages/undertow-component.adoc index a74cfc6..282c603 100644 --- a/docs/components/modules/ROOT/pages/undertow-component.adoc +++ b/docs/components/modules/ROOT/pages/undertow-component.adoc @@ -226,4 +226,8 @@ Java SPI (Service Provider Interfaces). If there is an object passed to componen as parameter `securityConfiguration` and provider accepts it. Provider will be used for authentication of all requests. +Property `requireServletContext` of security providers forces udertow server to start +with servlet context. There will be no servlet actually handled. This feature is meant only +for use with servlet filters, which needs servlet context for their functionality. + include::camel-spring-boot::page$undertow-starter.adoc[] diff --git a/docs/components/modules/ROOT/pages/undertow-spring-security-component.adoc b/docs/components/modules/ROOT/pages/undertow-spring-security-component.adoc new file mode 100644 index 0000000..1e1b7e0 --- /dev/null +++ b/docs/components/modules/ROOT/pages/undertow-spring-security-component.adoc @@ -0,0 +1,34 @@ +[[undertow-spring-security-component]] += Undertow Spring Security Security Provider +//THIS FILE IS COPIED: EDIT THE SOURCE FILE: +:page-source: components/camel-undertow-spring-security/src/main/docs/undertow-spring-security-component.adoc +//by hand +:since: 3.2 + +*Since Camel {since}* + +*OSGi is not supported* + +The Spring Security Provider provides Spring Security (5+) token bearer security over camel-undertow component. +To force camel-undertow to use spring security provider: +- Add spring security provider library on classpath. +- Provide instance of SpringSecurityConfiguration as `securityConfiguration` +parameter into camel-undertow component or provide both `securityConfiguration` and `securityProvider` +into camel-undertow component. +- Configure spring-security. + +Configuration has to provide all 3 security attributes: +[width="100%"] +|=== +| Name | Description | Type +| *securityFiler* | Provides security filter gained from configured spring security (5+). Filter could be obtained +for example from DelegatingFilterProxyRegistrationBean. | Filter +| *clieantRegistration* | Provides configuration of external security provider which would be used by spring security | +ClientRegistration +|=== + +Each exchange created by Undertow endpoint with spring security contains header 'SpringSecurityProvider_principal' ( +name of header is provided as constant `SpringSecurityProvider.PRINCIPAL_NAME_HEADER`) with current authorized identity +as value or is not present (for not authorized requests) + + diff --git a/parent/pom.xml b/parent/pom.xml index 4778c68..12f57e0 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -2368,6 +2368,11 @@ </dependency> <dependency> <groupId>org.apache.camel</groupId> + <artifactId>camel-undertow-spring-security</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> <artifactId>camel-univocity-parsers</artifactId> <version>${project.version}</version> </dependency>