implement OpenAPI contributor integration for rate limiting and response headers
This commit is contained in:
+34
-32
@@ -263,62 +263,64 @@ public class OidcExtension implements FlashExtension {
|
||||
|
||||
/**
|
||||
* Loaded lazily so that {@code flash-ext-openapi} classes are only resolved at
|
||||
* runtime when {@link dev.relism.ext.openapi.OpenApiSecurityRegistry} is actually on the classpath.
|
||||
* runtime when {@link dev.relism.ext.openapi.OpenApiContributorRegistry} is actually on the classpath.
|
||||
*/
|
||||
private static final class OpenApiIntegration {
|
||||
static void register(dev.relism.extension.FlashContext ctx,
|
||||
OidcConfig config, OidcProviderMetadata meta) {
|
||||
ctx.find(dev.relism.ext.openapi.OpenApiSecurityRegistry.class)
|
||||
.ifPresent(registry -> registry.add(new dev.relism.ext.openapi.OpenApiSecurityContributor() {
|
||||
ctx.find(dev.relism.ext.openapi.OpenApiContributorRegistry.class)
|
||||
.ifPresent(registry -> registry.add(new dev.relism.ext.openapi.OpenApiContributor() {
|
||||
|
||||
@Override
|
||||
public String schemeName() { return config.schemeName(); }
|
||||
|
||||
@Override
|
||||
public java.util.Map<String, Object> schemeDefinition() {
|
||||
java.util.Map<String, String> scopesMap = new java.util.LinkedHashMap<>();
|
||||
for (String s : config.scopes().split("\\s+")) {
|
||||
if (!s.isBlank()) scopesMap.put(s, s);
|
||||
@Override
|
||||
public java.util.Map<String, Object> componentContributions() {
|
||||
java.util.Map<String, String> scopesMap = new java.util.LinkedHashMap<>();
|
||||
for (String s : config.scopes().split("\\s+")) {
|
||||
if (!s.isBlank()) scopesMap.put(s, s);
|
||||
}
|
||||
java.util.Map<String, Object> flow = new java.util.LinkedHashMap<>();
|
||||
flow.put("authorizationUrl", meta.authorizationEndpoint());
|
||||
flow.put("tokenUrl", meta.tokenEndpoint());
|
||||
flow.put("scopes", scopesMap);
|
||||
flow.put("scopes", scopesMap);
|
||||
|
||||
java.util.Map<String, Object> scheme = new java.util.LinkedHashMap<>();
|
||||
scheme.put("type", "oauth2");
|
||||
scheme.put("flows", java.util.Map.of("authorizationCode", flow));
|
||||
return scheme;
|
||||
}
|
||||
java.util.Map<String, Object> scheme = new java.util.LinkedHashMap<>();
|
||||
scheme.put("type", "oauth2");
|
||||
scheme.put("flows", java.util.Map.of("authorizationCode", flow));
|
||||
|
||||
@Override
|
||||
public java.util.List<String> requiredFor(Class<?> handlerClass) {
|
||||
return OidcAuthPolicy.openApiScopesFor(handlerClass);
|
||||
java.util.Map<String, Object> securitySchemes = new java.util.LinkedHashMap<>();
|
||||
securitySchemes.put(config.schemeName(), scheme);
|
||||
return java.util.Map.of("securitySchemes", securitySchemes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<Integer, String> autoResponsesFor(Class<?> handlerClass) {
|
||||
OidcAuthPolicy policy = OidcAuthPolicy.compileFromAnnotations(handlerClass);
|
||||
if (policy == null || policy.optionalAuth()) return java.util.Map.of();
|
||||
@Override
|
||||
public dev.relism.ext.openapi.OpenApiOperationContribution operationFor(Class<?> handlerClass) {
|
||||
dev.relism.ext.openapi.OpenApiOperationContribution.Builder out =
|
||||
dev.relism.ext.openapi.OpenApiOperationContribution.builder();
|
||||
|
||||
java.util.LinkedHashMap<Integer, String> out = new java.util.LinkedHashMap<>();
|
||||
out.put(401, "Authentication required");
|
||||
java.util.List<String> operationScopes = OidcAuthPolicy.openApiScopesFor(handlerClass);
|
||||
if (operationScopes != null) {
|
||||
out.security(config.schemeName(), operationScopes);
|
||||
}
|
||||
|
||||
OidcAuthPolicy policy = OidcAuthPolicy.compileFromAnnotations(handlerClass);
|
||||
if (policy == null || policy.optionalAuth()) return out.build();
|
||||
|
||||
out.response(401, dev.relism.ext.openapi.OpenApiResponseContribution.of("Authentication required"));
|
||||
|
||||
String[] roles = policy.requiredRoles();
|
||||
String[] scopes = policy.requiredScopes();
|
||||
if (roles.length == 0 && scopes.length == 0) return out;
|
||||
if (roles.length == 0 && scopes.length == 0) return out.build();
|
||||
|
||||
String roleMessage = roles.length == 0 ? null : roleRequiredMessage(roles);
|
||||
String scopeMessage = scopes.length == 0 ? null : scopeRequiredMessage(scopes);
|
||||
if (roleMessage != null && scopeMessage != null) {
|
||||
out.put(403, roleMessage + "; " + scopeMessage);
|
||||
out.response(403, dev.relism.ext.openapi.OpenApiResponseContribution.of(roleMessage + "; " + scopeMessage));
|
||||
} else if (roleMessage != null) {
|
||||
out.put(403, roleMessage);
|
||||
out.response(403, dev.relism.ext.openapi.OpenApiResponseContribution.of(roleMessage));
|
||||
} else {
|
||||
out.put(403, scopeMessage);
|
||||
out.response(403, dev.relism.ext.openapi.OpenApiResponseContribution.of(scopeMessage));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return out.build();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+29
-11
@@ -1,10 +1,11 @@
|
||||
package dev.relism.ext.oidc;
|
||||
|
||||
import dev.relism.ext.openapi.OpenApiSecurityContributor;
|
||||
import dev.relism.ext.openapi.OpenApiContributor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -37,49 +38,57 @@ class OidcOpenApiInteropTest {
|
||||
|
||||
@Test
|
||||
void autoResponses_authOnly() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(AuthOnly.class);
|
||||
Map<Integer, String> responses = responses(AuthOnly.class);
|
||||
assertEquals("Authentication required", responses.get(401));
|
||||
assertFalse(responses.containsKey(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_optionalAuth_addsNothing() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(AuthOptional.class);
|
||||
Map<Integer, String> responses = responses(AuthOptional.class);
|
||||
assertTrue(responses.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_oneRole_formatsSingular() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(OneRole.class);
|
||||
Map<Integer, String> responses = responses(OneRole.class);
|
||||
assertEquals("Authentication required", responses.get(401));
|
||||
assertEquals("\"admin\" role required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_multiRoles_formatsPlural() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(MultiRole.class);
|
||||
Map<Integer, String> responses = responses(MultiRole.class);
|
||||
assertEquals("Roles \"admin, operator\" are required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_oneScope_formatsSingular() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(OneScope.class);
|
||||
Map<Integer, String> responses = responses(OneScope.class);
|
||||
assertEquals("\"orders:write\" scope required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_multiScopes_formatsPlural() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(MultiScope.class);
|
||||
Map<Integer, String> responses = responses(MultiScope.class);
|
||||
assertEquals("Scopes \"orders:write, payments:write\" are required", responses.get(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoResponses_roleAndScope_combinesMessages() throws Exception {
|
||||
Map<Integer, String> responses = contributor().autoResponsesFor(RoleAndScope.class);
|
||||
Map<Integer, String> responses = responses(RoleAndScope.class);
|
||||
assertEquals("\"admin\" role required; \"orders:write\" scope required", responses.get(403));
|
||||
}
|
||||
|
||||
private static OpenApiSecurityContributor contributor() throws Exception {
|
||||
@Test
|
||||
void securityContribution_presentForAuthenticatedHandler() throws Exception {
|
||||
dev.relism.ext.openapi.OpenApiOperationContribution operation = contributor().operationFor(AuthOnly.class);
|
||||
List<Map<String, List<String>>> security = operation.security();
|
||||
assertEquals(1, security.size());
|
||||
assertTrue(security.getFirst().containsKey("issuer"));
|
||||
}
|
||||
|
||||
private static OpenApiContributor contributor() throws Exception {
|
||||
Class<?> clazz = Class.forName("dev.relism.ext.oidc.OidcExtension$OpenApiIntegration");
|
||||
Constructor<?> ctor = clazz.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
@@ -89,8 +98,8 @@ class OidcOpenApiInteropTest {
|
||||
m.setAccessible(true);
|
||||
|
||||
dev.relism.extension.FlashContext ctx = new dev.relism.extension.FlashContext();
|
||||
dev.relism.ext.openapi.OpenApiSecurityRegistry registry = new dev.relism.ext.openapi.OpenApiSecurityRegistry();
|
||||
ctx.provide(dev.relism.ext.openapi.OpenApiSecurityRegistry.class, registry);
|
||||
dev.relism.ext.openapi.OpenApiContributorRegistry registry = new dev.relism.ext.openapi.OpenApiContributorRegistry();
|
||||
ctx.provide(dev.relism.ext.openapi.OpenApiContributorRegistry.class, registry);
|
||||
|
||||
OidcConfig config = OidcConfig.builder("https://issuer", "c", "s", "/cb").build();
|
||||
OidcProviderMetadata meta = new OidcProviderMetadata("a", "t", "u", "j", "e");
|
||||
@@ -98,4 +107,13 @@ class OidcOpenApiInteropTest {
|
||||
|
||||
return registry.contributors().getFirst();
|
||||
}
|
||||
|
||||
private static Map<Integer, String> responses(Class<?> cls) throws Exception {
|
||||
Map<Integer, dev.relism.ext.openapi.OpenApiResponseContribution> byCode = contributor().operationFor(cls).responses();
|
||||
java.util.LinkedHashMap<Integer, String> out = new java.util.LinkedHashMap<>();
|
||||
for (Map.Entry<Integer, dev.relism.ext.openapi.OpenApiResponseContribution> e : byCode.entrySet()) {
|
||||
out.put(e.getKey(), e.getValue().description());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user