Skip to content

Commit 8e82dcc

Browse files
committed
feat: Fail fast at boot if security validators are missing
Builder.build() now calls validateSecurityWiring() before constructing the server when externalAuth is false. It collects all scheme names referenced by any operation's effective security, and throws IllegalStateException for unknown, Unsupported, or validator-less schemes.
1 parent dbd03c9 commit 8e82dcc

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

src/main/java/com/retailsvc/http/OpenApiServer.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import com.retailsvc.http.internal.TextTypeMapper;
1515
import com.retailsvc.http.spec.Operation;
1616
import com.retailsvc.http.spec.Spec;
17+
import com.retailsvc.http.spec.security.SecurityRequirement;
18+
import com.retailsvc.http.spec.security.SecurityScheme;
1719
import com.retailsvc.http.validate.DefaultValidator;
1820
import com.sun.net.httpserver.HttpContext;
1921
import com.sun.net.httpserver.HttpHandler;
@@ -22,10 +24,12 @@
2224
import java.net.InetSocketAddress;
2325
import java.util.ArrayList;
2426
import java.util.LinkedHashMap;
27+
import java.util.LinkedHashSet;
2528
import java.util.List;
2629
import java.util.Locale;
2730
import java.util.Map;
2831
import java.util.Optional;
32+
import java.util.Set;
2933
import java.util.stream.Collectors;
3034
import org.slf4j.Logger;
3135
import org.slf4j.LoggerFactory;
@@ -274,6 +278,9 @@ public OpenApiServer build() throws IOException {
274278
"extra handler path " + path + " conflicts with spec basePath " + basePath);
275279
}
276280
}
281+
if (!externalAuth) {
282+
validateSecurityWiring(spec, securityValidators);
283+
}
277284
Map<String, TypeMapper> resolved = resolveBodyMappers(bodyMappers);
278285
HandlerConfig handlerConfig =
279286
new HandlerConfig(
@@ -287,6 +294,30 @@ public OpenApiServer build() throws IOException {
287294
return new OpenApiServer(spec, resolved, handlerConfig, port, shutdownTimeoutSeconds);
288295
}
289296

297+
private static void validateSecurityWiring(Spec spec, Map<String, SchemeValidator> validators) {
298+
Set<String> referenced = new LinkedHashSet<>();
299+
for (Operation op : spec.operations()) {
300+
for (SecurityRequirement req : op.security().orElse(spec.security())) {
301+
referenced.addAll(req.schemes().keySet());
302+
}
303+
}
304+
for (String name : referenced) {
305+
SecurityScheme scheme = spec.securitySchemes().get(name);
306+
if (scheme == null) {
307+
throw new IllegalStateException(
308+
"security requirement references unknown scheme '" + name + "'");
309+
}
310+
if (scheme instanceof SecurityScheme.Unsupported u) {
311+
throw new IllegalStateException(
312+
"scheme '" + name + "' uses unsupported type '" + u.type() + "'");
313+
}
314+
if (!validators.containsKey(name)) {
315+
throw new IllegalStateException(
316+
"no SchemeValidator registered for security scheme '" + name + "'");
317+
}
318+
}
319+
}
320+
290321
private static Map<String, TypeMapper> resolveBodyMappers(
291322
Map<String, TypeMapper> userSupplied) {
292323
LinkedHashMap<String, TypeMapper> out = new LinkedHashMap<>();
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.retailsvc.http;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import com.retailsvc.http.spec.Spec;
7+
import java.util.List;
8+
import java.util.Map;
9+
import org.junit.jupiter.api.Test;
10+
11+
class SecurityBootValidationTest {
12+
13+
private static Map<String, Object> raw(
14+
Map<String, Object> securitySchemes, List<Object> rootSecurity, List<Object> opSecurity) {
15+
return Map.of(
16+
"openapi",
17+
"3.1.0",
18+
"info",
19+
Map.of("title", "T", "version", "1"),
20+
"servers",
21+
List.of(Map.of("url", "/v1")),
22+
"security",
23+
rootSecurity == null ? List.of() : rootSecurity,
24+
"components",
25+
securitySchemes == null ? Map.of() : Map.of("securitySchemes", securitySchemes),
26+
"paths",
27+
Map.of(
28+
"/x",
29+
Map.of(
30+
"get",
31+
opSecurity == null
32+
? Map.of(
33+
"operationId",
34+
"getX",
35+
"responses",
36+
Map.of("200", Map.of("description", "ok")))
37+
: Map.of(
38+
"operationId",
39+
"getX",
40+
"security",
41+
opSecurity,
42+
"responses",
43+
Map.of("200", Map.of("description", "ok"))))));
44+
}
45+
46+
@Test
47+
void missingValidatorThrows() {
48+
Map<String, Object> r =
49+
raw(
50+
Map.of("bearerAuth", Map.of("type", "http", "scheme", "bearer")),
51+
List.of(),
52+
List.of(Map.of("bearerAuth", List.of())));
53+
Spec spec = Spec.from(r);
54+
55+
assertThatThrownBy(
56+
() ->
57+
OpenApiServer.builder()
58+
.spec(spec)
59+
.handlers(Map.of("getX", req -> Response.ok(Map.of())))
60+
.port(0)
61+
.build())
62+
.isInstanceOf(IllegalStateException.class)
63+
.hasMessageContaining("bearerAuth");
64+
}
65+
66+
@Test
67+
void unsupportedSchemeThrowsWhenReferenced() {
68+
Map<String, Object> r =
69+
raw(
70+
Map.of("oauth", Map.of("type", "oauth2")),
71+
List.of(),
72+
List.of(Map.of("oauth", List.of())));
73+
Spec spec = Spec.from(r);
74+
75+
assertThatThrownBy(
76+
() ->
77+
OpenApiServer.builder()
78+
.spec(spec)
79+
.handlers(Map.of("getX", req -> Response.ok(Map.of())))
80+
.port(0)
81+
.build())
82+
.isInstanceOf(IllegalStateException.class)
83+
.hasMessageContaining("unsupported");
84+
}
85+
86+
@Test
87+
void unknownSchemeReferenceThrows() {
88+
Map<String, Object> r =
89+
raw(
90+
Map.of(), // no schemes defined
91+
List.of(),
92+
List.of(Map.of("missingScheme", List.of())));
93+
Spec spec = Spec.from(r);
94+
95+
assertThatThrownBy(
96+
() ->
97+
OpenApiServer.builder()
98+
.spec(spec)
99+
.handlers(Map.of("getX", req -> Response.ok(Map.of())))
100+
.port(0)
101+
.build())
102+
.isInstanceOf(IllegalStateException.class)
103+
.hasMessageContaining("missingScheme");
104+
}
105+
106+
@Test
107+
void externalAuthSkipsAllChecks() throws Exception {
108+
Map<String, Object> r =
109+
raw(
110+
Map.of("bearerAuth", Map.of("type", "http", "scheme", "bearer")),
111+
List.of(),
112+
List.of(Map.of("bearerAuth", List.of())));
113+
Spec spec = Spec.from(r);
114+
115+
// No validator registered, but externalAuth → must succeed.
116+
OpenApiServer server =
117+
OpenApiServer.builder()
118+
.spec(spec)
119+
.useExternalAuthentication()
120+
.handlers(Map.of("getX", req -> Response.ok(Map.of())))
121+
.port(0)
122+
.build();
123+
124+
assertThat(server).isNotNull();
125+
server.close();
126+
}
127+
}

0 commit comments

Comments
 (0)