i spent the last year just spinning

This commit is contained in:
Relism
2026-04-17 18:56:06 +02:00
parent 9efbe38c0c
commit e161497f2c
77 changed files with 2545 additions and 877 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>dev.relism</groupId>
<artifactId>flash-extensions</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.1-indev5</version>
</parent>
<artifactId>flash-ext-oidc</artifactId>
@@ -295,7 +295,41 @@ public class OidcExtension implements FlashExtension {
public java.util.List<String> requiredFor(Class<?> handlerClass) {
return OidcAuthPolicy.openApiScopesFor(handlerClass);
}
@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();
java.util.LinkedHashMap<Integer, String> out = new java.util.LinkedHashMap<>();
out.put(401, "Authentication required");
String[] roles = policy.requiredRoles();
String[] scopes = policy.requiredScopes();
if (roles.length == 0 && scopes.length == 0) return out;
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);
} else if (roleMessage != null) {
out.put(403, roleMessage);
} else {
out.put(403, scopeMessage);
}
return out;
}
}));
}
private static String roleRequiredMessage(String[] roles) {
if (roles.length == 1) return "\"" + roles[0] + "\" role required";
return "Roles \"" + String.join(", ", roles) + "\" are required";
}
private static String scopeRequiredMessage(String[] scopes) {
if (scopes.length == 1) return "\"" + scopes[0] + "\" scope required";
return "Scopes \"" + String.join(", ", scopes) + "\" are required";
}
}
}
@@ -0,0 +1,101 @@
package dev.relism.ext.oidc;
import dev.relism.ext.openapi.OpenApiSecurityContributor;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class OidcOpenApiInteropTest {
@Authenticated
static class AuthOnly {}
@Authenticated(optional = true)
static class AuthOptional {}
@RolesAllowed("admin")
static class OneRole {}
@RolesAllowed({"admin", "operator"})
static class MultiRole {}
@ScopesAllowed("orders:write")
static class OneScope {}
@ScopesAllowed({"orders:write", "payments:write"})
static class MultiScope {}
@RolesAllowed("admin")
@ScopesAllowed("orders:write")
static class RoleAndScope {}
@Test
void autoResponses_authOnly() throws Exception {
Map<Integer, String> responses = contributor().autoResponsesFor(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);
assertTrue(responses.isEmpty());
}
@Test
void autoResponses_oneRole_formatsSingular() throws Exception {
Map<Integer, String> responses = contributor().autoResponsesFor(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);
assertEquals("Roles \"admin, operator\" are required", responses.get(403));
}
@Test
void autoResponses_oneScope_formatsSingular() throws Exception {
Map<Integer, String> responses = contributor().autoResponsesFor(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);
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);
assertEquals("\"admin\" role required; \"orders:write\" scope required", responses.get(403));
}
private static OpenApiSecurityContributor contributor() throws Exception {
Class<?> clazz = Class.forName("dev.relism.ext.oidc.OidcExtension$OpenApiIntegration");
Constructor<?> ctor = clazz.getDeclaredConstructor();
ctor.setAccessible(true);
Object instance = ctor.newInstance();
Method m = clazz.getDeclaredMethod("register", dev.relism.extension.FlashContext.class, OidcConfig.class, OidcProviderMetadata.class);
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);
OidcConfig config = OidcConfig.builder("https://issuer", "c", "s", "/cb").build();
OidcProviderMetadata meta = new OidcProviderMetadata("a", "t", "u", "j", "e");
m.invoke(instance, ctx, config, meta);
return registry.contributors().getFirst();
}
}