diff --git a/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityFilterCommon.java b/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityFilterCommon.java index 63bc7ffb4c3..bc1e5dc25ef 100644 --- a/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityFilterCommon.java +++ b/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityFilterCommon.java @@ -19,8 +19,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.ServiceLoader; import java.util.logging.Logger; +import io.helidon.common.serviceloader.HelidonServiceLoader; import io.helidon.config.Config; import io.helidon.security.AuthenticationResponse; import io.helidon.security.AuthorizationResponse; @@ -47,6 +49,9 @@ abstract class SecurityFilterCommon { static final String PROP_FILTER_CONTEXT = "io.helidon.security.jersey.FilterContext"; + private static final List RESPONSE_MAPPERS = HelidonServiceLoader + .builder(ServiceLoader.load(SecurityResponseMapper.class)).build().asList(); + @Context private Security security; @@ -343,7 +348,10 @@ protected void abortRequest(SecurityFilter.FilterContext context, updateHeaders(responseHeaders, responseBuilder); } - if (featureConfig.isDebug()) { + // Run security response mappers if available, or revert to old logic for compatibility + if (!RESPONSE_MAPPERS.isEmpty()) { + RESPONSE_MAPPERS.forEach(m -> m.aborted(response, responseBuilder)); + } else if (featureConfig.isDebug()) { response.description().ifPresent(responseBuilder::entity); } diff --git a/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityResponseMapper.java b/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityResponseMapper.java new file mode 100644 index 00000000000..0ae0881ff45 --- /dev/null +++ b/security/integration/jersey/src/main/java/io/helidon/security/integration/jersey/SecurityResponseMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.integration.jersey; + +import io.helidon.common.context.Contexts; +import io.helidon.security.SecurityResponse; + +import jakarta.ws.rs.core.Response; + +/** + * A {@link SecurityResponse} mapper that is called when a security error is + * encountered. Gives a chance for applications to craft a more informative + * response to the user as to the cause of the error. + */ +@FunctionalInterface +public interface SecurityResponseMapper { + + /** + * Called when a security response is aborted due to a security problem (e.g. authentication + * failure). Handles control to the application to construct the response returned to + * the client. Security providers can provide context to mappers using the Helidon + * context mechanism. + * + * @param securityResponse the security response + * @param responseBuilder the response builder + * @see Contexts#context() + */ + void aborted(SecurityResponse securityResponse, Response.ResponseBuilder responseBuilder); +} diff --git a/security/integration/jersey/src/main/java/module-info.java b/security/integration/jersey/src/main/java/module-info.java index 8df4afd4695..49650ef7797 100644 --- a/security/integration/jersey/src/main/java/module-info.java +++ b/security/integration/jersey/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021 Oracle and/or its affiliates. + * Copyright (c) 2017, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,4 +44,5 @@ opens io.helidon.security.integration.jersey to hk2.locator,hk2.utils,weld.core.impl, io.helidon.microprofile.cdi; uses io.helidon.security.providers.common.spi.AnnotationAnalyzer; + uses io.helidon.security.integration.jersey.SecurityResponseMapper; } diff --git a/tests/integration/security/pom.xml b/tests/integration/security/pom.xml index 9be3d76bd69..30db5f2a5de 100644 --- a/tests/integration/security/pom.xml +++ b/tests/integration/security/pom.xml @@ -1,6 +1,6 @@ + + + + helidon-tests-integration-security + io.helidon.tests.integration + 3.0.0-SNAPSHOT + + 4.0.0 + + helidon-tests-integration-security-response-mapper + Helidon Tests Integration Security Response Mappers + + + Integration test for security response mappers + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + org.jboss + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + + + diff --git a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/MySecurityResponseMapper.java b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/MySecurityResponseMapper.java new file mode 100644 index 00000000000..6c4d32aad76 --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/MySecurityResponseMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.mapper; + +import jakarta.ws.rs.core.Response; + +import io.helidon.common.context.Contexts; +import io.helidon.security.SecurityResponse; +import io.helidon.security.integration.jersey.SecurityResponseMapper; + +/** + * Mapper that intercepts creation of {@link Response} when a security + * error is encountered. + */ +public class MySecurityResponseMapper implements SecurityResponseMapper { + + @Override + public void aborted(SecurityResponse securityResponse, Response.ResponseBuilder responseBuilder) { + responseBuilder.header("MAPPED-BY", "MySecurityResponseMapper"); + + // Access data set in RestrictedProvider + Contexts.context() + .flatMap(c -> c.get(RestrictedProvider.class, String.class)) + .ifPresent(p -> responseBuilder.header("PROVIDER", p)); + } +} diff --git a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/Restricted.java b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/Restricted.java new file mode 100644 index 00000000000..1366e29799a --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/Restricted.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +@Documented +@Inherited +public @interface Restricted { +} diff --git a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java new file mode 100644 index 00000000000..1508c442ea2 --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.mapper; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; + +import io.helidon.common.context.Contexts; +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.ProviderRequest; +import io.helidon.security.spi.AuthenticationProvider; +import io.helidon.security.spi.SynchronousProvider; + +public class RestrictedProvider extends SynchronousProvider implements AuthenticationProvider { + + /** + * Register an entry in {@link io.helidon.common.context.Context} and fail authentication. + * + * @param providerRequest provider request + * @return authentication response + */ + @Override + protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + // Use context to communicate with MySecurityResponseMapper + Contexts.context() + .ifPresent(c -> c.register(RestrictedProvider.class, getClass().getSimpleName())); + return AuthenticationResponse.failed("Oops"); + + } + + @Override + public Collection> supportedAnnotations() { + return List.of(Restricted.class); + } +} diff --git a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProviderService.java b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProviderService.java new file mode 100644 index 00000000000..5b2d118d6cf --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProviderService.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.mapper; + +import io.helidon.config.Config; +import io.helidon.security.spi.SecurityProvider; +import io.helidon.security.spi.SecurityProviderService; + +public class RestrictedProviderService implements SecurityProviderService { + + @Override + public String providerConfigKey() { + return "restricted"; + } + + @Override + public Class providerClass() { + return RestrictedProvider.class; + } + + @Override + public SecurityProvider providerInstance(Config config) { + return new RestrictedProvider(); + } +} diff --git a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedResource.java b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedResource.java new file mode 100644 index 00000000000..e80e459e6ba --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedResource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.mapper; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.helidon.security.annotations.Authenticated; + +@Path("restricted") +@Authenticated +public class RestrictedResource { + + @GET + @Restricted + public String restricted() { + return "Never called"; + } +} diff --git a/tests/integration/security/security-response-mapper/src/main/resources/META-INF/services/io.helidon.security.integration.jersey.SecurityResponseMapper b/tests/integration/security/security-response-mapper/src/main/resources/META-INF/services/io.helidon.security.integration.jersey.SecurityResponseMapper new file mode 100644 index 00000000000..57b1e59a9ab --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/resources/META-INF/services/io.helidon.security.integration.jersey.SecurityResponseMapper @@ -0,0 +1 @@ +io.helidon.tests.integration.security.mapper.MySecurityResponseMapper \ No newline at end of file diff --git a/tests/integration/security/security-response-mapper/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService b/tests/integration/security/security-response-mapper/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService new file mode 100644 index 00000000000..2692d5133df --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService @@ -0,0 +1 @@ +io.helidon.tests.integration.security.mapper.RestrictedProviderService \ No newline at end of file diff --git a/tests/integration/security/security-response-mapper/src/main/resources/application.yaml b/tests/integration/security/security-response-mapper/src/main/resources/application.yaml new file mode 100644 index 00000000000..d465c314771 --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: -1 + +security: + providers: + - restricted: diff --git a/tests/integration/security/security-response-mapper/src/main/resources/logging.properties b/tests/integration/security/security-response-mapper/src/main/resources/logging.properties new file mode 100644 index 00000000000..998f7fd29e2 --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/main/resources/logging.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.common.HelidonConsoleHandler + +# Global logging level. Can be overridden by specific loggers +.level=WARNING + +AUDIT.level=FINEST diff --git a/tests/integration/security/security-response-mapper/src/test/java/io/helidon/tests/integration/security/mapper/RestrictedResourceTest.java b/tests/integration/security/security-response-mapper/src/test/java/io/helidon/tests/integration/security/mapper/RestrictedResourceTest.java new file mode 100644 index 00000000000..89e68f42c62 --- /dev/null +++ b/tests/integration/security/security-response-mapper/src/test/java/io/helidon/tests/integration/security/mapper/RestrictedResourceTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.mapper; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; + +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.HelidonTest; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddBean(RestrictedResource.class) +class RestrictedResourceTest { + + @Inject + private WebTarget webTarget; + + @Test + void testRestricted() { + Response response = webTarget.path("restricted") + .request() + .get(); + + assertThat(response.getStatus(), is(401)); + assertThat(response.getHeaderString("PROVIDER"), is("RestrictedProvider")); + assertThat(response.getHeaderString("MAPPED-BY"), is("MySecurityResponseMapper")); + } +} \ No newline at end of file