Skip to content

Commit aab69ec

Browse files
tvallinromain-grecourt
authored andcommitted
metrics and openapi endpoint authorized
Signed-off-by: tvallin <[email protected]>
1 parent a84fd42 commit aab69ec

File tree

10 files changed

+294
-7
lines changed

10 files changed

+294
-7
lines changed

metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java

+16
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ static List<Tag> createTags(String pairs) {
129129
@ConfiguredOption("true")
130130
boolean enabled();
131131

132+
/**
133+
* Whether metrics endpoint should be authorized.
134+
*
135+
* @return if metrics are configured to be authorized
136+
*/
137+
@ConfiguredOption
138+
boolean permitAll();
139+
140+
/**
141+
* Hints for role names the user is expected to be in.
142+
*
143+
* @return list of hints
144+
*/
145+
@ConfiguredOption
146+
List<String> roles();
147+
132148
/**
133149
* Key performance indicator metrics settings.
134150
*

microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java

+13-6
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,19 @@ public void filter(ContainerRequestContext request) {
6666
SecurityTracing tracing = SecurityTracing.get();
6767

6868
// create a new security context
69-
SecurityContext securityContext = security()
70-
.contextBuilder(Integer.toString(CONTEXT_COUNTER.incrementAndGet(), Character.MAX_RADIX))
71-
.tracingSpan(tracing.findParent().orElse(null))
72-
.build();
73-
74-
Contexts.context().ifPresent(ctx -> ctx.register(securityContext));
69+
SecurityContext securityContext = Contexts.context()
70+
.flatMap(context -> context.get(SecurityContext.class))
71+
.orElse(null);
72+
73+
if (securityContext == null) {
74+
// create a new security context
75+
securityContext = security()
76+
.contextBuilder(Integer.toString(CONTEXT_COUNTER.incrementAndGet(), Character.MAX_RADIX))
77+
.tracingSpan(tracing.findParent().orElse(null))
78+
.build();
79+
SecurityContext finalSecurityContext = securityContext;
80+
Contexts.context().ifPresent(ctx -> ctx.register(finalSecurityContext));
81+
}
7582

7683
injectionManager.<Ref<SecurityContext>>getInstance((new GenericType<Ref<SecurityContext>>() { }).getType())
7784
.set(securityContext);

openapi/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.helidon.http.Status;
3939
import io.helidon.webserver.cors.CorsEnabledServiceHelper;
4040
import io.helidon.webserver.http.HttpRouting;
41+
import io.helidon.webserver.http.SecureHandler;
4142
import io.helidon.webserver.http.ServerRequest;
4243
import io.helidon.webserver.http.ServerResponse;
4344
import io.helidon.webserver.servicecommon.FeatureSupport;
@@ -159,6 +160,9 @@ public OpenApiFeatureConfig prototype() {
159160

160161
@Override
161162
public void setup(HttpRouting.Builder routing, HttpRouting.Builder featureRouting) {
163+
if (!config.permitAll()) {
164+
routing.any(SecureHandler.authorize(config.roles().toArray(new String[0])));
165+
}
162166
String path = prototype().webContext();
163167
routing.any(path, corsService.processor())
164168
.get(path, this::handle);

openapi/openapi/src/main/java/io/helidon/openapi/OpenApiFeatureConfigBlueprint.java

+16
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,20 @@ interface OpenApiFeatureConfigBlueprint extends Prototype.Factory<OpenApiFeature
8282
*/
8383
@ConfiguredOption(provider = true, providerType = OpenApiManagerProvider.class, providerDiscoverServices = false)
8484
Optional<OpenApiManager<?>> manager();
85+
86+
/**
87+
* Whether endpoint should be authorized.
88+
*
89+
* @return if endpoint is configured to be authorized
90+
*/
91+
@ConfiguredOption
92+
boolean permitAll();
93+
94+
/**
95+
* Hints for role names the user is expected to be in.
96+
*
97+
* @return list of hints
98+
*/
99+
@ConfiguredOption
100+
List<String> roles();
85101
}

webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import io.helidon.webserver.http.HttpRouting;
4040
import io.helidon.webserver.http.HttpRules;
4141
import io.helidon.webserver.http.HttpService;
42+
import io.helidon.webserver.http.SecureHandler;
4243
import io.helidon.webserver.http.ServerRequest;
4344
import io.helidon.webserver.http.ServerResponse;
4445

@@ -194,6 +195,9 @@ private void getOrOptionsMatching(MediaType mediaType,
194195
}
195196

196197
private void setUpEndpoints(HttpRules rules) {
198+
if (!metricsConfig.permitAll()) {
199+
rules.any(SecureHandler.authorize(metricsConfig.roles().toArray(new String[0])));
200+
}
197201
// routing to root of metrics
198202
// As of Helidon 4, this is the only path we should need because scope-based or metric-name-based
199203
// selection should use query parameters instead of paths.

webserver/tests/observe/pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@
3636
<modules>
3737
<module>health</module>
3838
<module>observe</module>
39+
<module>security</module>
3940
</modules>
4041
</project>
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright (c) 2023 Oracle and/or its affiliates.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
18+
xmlns="http://maven.apache.org/POM/4.0.0"
19+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
20+
<modelVersion>4.0.0</modelVersion>
21+
<parent>
22+
<groupId>io.helidon.webserver.tests.observe</groupId>
23+
<artifactId>helidon-webserver-tests-observe-project</artifactId>
24+
<version>4.0.0-SNAPSHOT</version>
25+
</parent>
26+
27+
<artifactId>helidon-webserver-observe-tests-security</artifactId>
28+
<name>Helidon WebServer Tests Observe Metrics</name>
29+
30+
<dependencies>
31+
<dependency>
32+
<groupId>io.helidon.webserver</groupId>
33+
<artifactId>helidon-webserver</artifactId>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>io.helidon.security.providers</groupId>
38+
<artifactId>helidon-security-providers-http-auth</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
<dependency>
42+
<groupId>io.helidon.webserver.observe</groupId>
43+
<artifactId>helidon-webserver-observe-metrics</artifactId>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>io.helidon.openapi</groupId>
48+
<artifactId>helidon-openapi</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>io.helidon.webserver.testing.junit5</groupId>
53+
<artifactId>helidon-webserver-testing-junit5</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.junit.jupiter</groupId>
58+
<artifactId>junit-jupiter-api</artifactId>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>org.hamcrest</groupId>
63+
<artifactId>hamcrest-all</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
<dependency>
67+
<groupId>io.helidon.webserver</groupId>
68+
<artifactId>helidon-webserver-security</artifactId>
69+
<scope>test</scope>
70+
</dependency>
71+
<dependency>
72+
<groupId>io.helidon.webclient</groupId>
73+
<artifactId>helidon-webclient-security</artifactId>
74+
<scope>test</scope>
75+
</dependency>
76+
</dependencies>
77+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.webserver.tests.observe.metrics;
18+
19+
import java.net.URI;
20+
import java.util.Arrays;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.Set;
25+
26+
import io.helidon.common.media.type.MediaTypes;
27+
import io.helidon.http.Status;
28+
import io.helidon.openapi.OpenApiFeature;
29+
import io.helidon.security.EndpointConfig;
30+
import io.helidon.security.Security;
31+
import io.helidon.security.providers.httpauth.HttpBasicAuthProvider;
32+
import io.helidon.security.providers.httpauth.SecureUserStore;
33+
import io.helidon.webclient.http1.Http1Client;
34+
import io.helidon.webclient.http1.Http1ClientResponse;
35+
import io.helidon.webclient.security.WebClientSecurity;
36+
import io.helidon.webserver.WebServerConfig;
37+
import io.helidon.webserver.context.ContextFeature;
38+
import io.helidon.webserver.observe.ObserveFeature;
39+
import io.helidon.webserver.security.SecurityFeature;
40+
import io.helidon.webserver.testing.junit5.ServerTest;
41+
import io.helidon.webserver.testing.junit5.SetUpServer;
42+
43+
import org.junit.jupiter.api.Test;
44+
45+
import static org.hamcrest.CoreMatchers.is;
46+
import static org.hamcrest.MatcherAssert.assertThat;
47+
48+
@ServerTest
49+
class ObserveSecurityTest {
50+
private static final Map<String, MyUser> USERS = new HashMap<>();
51+
52+
private final Http1Client client;
53+
54+
ObserveSecurityTest(URI uri) {
55+
USERS.put("jack", new MyUser("jack", "password".toCharArray(), Set.of("user")));
56+
57+
Security security = Security.builder()
58+
.addProvider(HttpBasicAuthProvider.builder())
59+
.build();
60+
61+
WebClientSecurity securityService = WebClientSecurity.create(security);
62+
63+
client = Http1Client.builder()
64+
.baseUri(uri)
65+
.addService(securityService)
66+
.build();
67+
}
68+
69+
@SetUpServer
70+
static void setup(WebServerConfig.Builder server) {
71+
server.routing(routing -> routing
72+
.addFeature(ObserveFeature.create())
73+
.addFeature(OpenApiFeature.builder().build())
74+
.addFeature(ContextFeature.create())
75+
.addFeature(buildWebSecurity().securityDefaults(SecurityFeature.authenticate()))
76+
.get("/observe/metrics", SecurityFeature.rolesAllowed("user"))
77+
.get("/openapi", SecurityFeature.rolesAllowed("user")));
78+
}
79+
80+
@Test
81+
void testMetrics() {
82+
testSecureEndpoint("/observe/metrics");
83+
}
84+
85+
@Test
86+
void testOpenApi() {
87+
testSecureEndpoint("/openapi");
88+
}
89+
90+
void testSecureEndpoint(String uri) {
91+
try (Http1ClientResponse response = client.get().uri(uri).request()) {
92+
assertThat(response.status(), is(Status.UNAUTHORIZED_401));
93+
}
94+
95+
try (Http1ClientResponse response = client.get()
96+
.uri(uri)
97+
.property(EndpointConfig.PROPERTY_OUTBOUND_ID, "jack")
98+
.property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, "password")
99+
.accept(MediaTypes.TEXT_PLAIN)
100+
.request()) {
101+
assertThat(response.status(), is(Status.OK_200));
102+
}
103+
}
104+
105+
private static SecurityFeature buildWebSecurity() {
106+
Security security = Security.builder()
107+
.addAuthenticationProvider(
108+
HttpBasicAuthProvider.builder()
109+
.realm("helidon")
110+
.userStore(buildUserStore()),
111+
"http-basic-auth")
112+
.build();
113+
return SecurityFeature.create(security);
114+
}
115+
116+
private static SecureUserStore buildUserStore() {
117+
return login -> Optional.ofNullable(USERS.get(login));
118+
}
119+
120+
private record MyUser(String login, char[] password, Set<String> roles) implements SecureUserStore.User {
121+
122+
@Override
123+
public boolean isPasswordValid(char[] password) {
124+
return Arrays.equals(password(), password);
125+
}
126+
}
127+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#
2+
# Copyright (c) 2023 Oracle and/or its affiliates.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
---
17+
# Currently openapi support only static document
18+
openapi: 3.0.0
19+
info:
20+
title: Helidon OpenApi Secure Tests
21+
description: A very simple application
22+
version: 1.0.0
23+
24+
servers:
25+
- url: http://localhost:8080
26+
description: Local test server
27+
28+
paths:
29+
/greet:
30+
get:
31+
summary: Returns a generic greeting
32+
description: Greets the user generically
33+
responses:
34+
default:
35+
description: Simple JSON containing the greeting

webserver/webserver/src/main/java/io/helidon/webserver/http/SecureHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static SecureHandler authenticate() {
5050
* Create a security handler that enforces authorization.
5151
*
5252
* @param roleHint optional hint for role names the user is expected to be in
53-
* @return a new handler that requires authroization
53+
* @return a new handler that requires authorization
5454
*/
5555
public static SecureHandler authorize(String... roleHint) {
5656
return new SecureHandler(false, true, roleHint);

0 commit comments

Comments
 (0)