This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch TOMEE-4730 in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 2d72ff24cf08b0d1b5df04a28ad445cf284ca3a4 Author: Richard Zowalla <[email protected]> AuthorDate: Fri Oct 9 10:18:56 2026 +0200 TOMEE-4730 apply class level security annotations of the declaring class Security annotations are not inherited, so methods inherited from an annotated base class were left unprotected in unannotated subclasses. --- ...PJWTSecurityAnnotationsInterceptorsFeature.java | 91 ++++++++++++---------- ...SecurityAnnotationsInterceptorsFeatureTest.java | 70 +++++++++++++++++ 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java index b4afaef9ff..cc9f1d6544 100644 --- a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java @@ -24,8 +24,8 @@ import jakarta.ws.rs.container.ResourceInfo; import jakarta.ws.rs.core.FeatureContext; import jakarta.ws.rs.ext.Provider; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -34,6 +34,9 @@ import java.util.Set; @Provider public class MPJWTSecurityAnnotationsInterceptorsFeature implements DynamicFeature { + private static final List<Class<? extends Annotation>> SECURITY_ANNOTATIONS = + Arrays.asList(RolesAllowed.class, PermitAll.class, DenyAll.class); + @Override public void configure(final ResourceInfo resourceInfo, final FeatureContext context) { @@ -48,65 +51,71 @@ public class MPJWTSecurityAnnotationsInterceptorsFeature implements DynamicFeatu } - private MPJWTSecurityAnnotationsInterceptor processSecurityAnnotations(final Class clazz, final Method method) { - - final List<Class<? extends Annotation>[]> classSecurityAnnotations = hasClassLevelAnnotations(clazz, - RolesAllowed.class, PermitAll.class, DenyAll.class); + private MPJWTSecurityAnnotationsInterceptor processSecurityAnnotations(final Class<?> resourceClass, final Method method) { - final List<Class<? extends Annotation>[]> methodSecurityAnnotations = hasMethodLevelAnnotations(method, - RolesAllowed.class, PermitAll.class, DenyAll.class); + if (countSecurityAnnotations(resourceClass) > 1) { + throw new IllegalStateException(resourceClass.getName() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); + } - if (classSecurityAnnotations.isEmpty() && methodSecurityAnnotations.isEmpty()) { - return null; // nothing to do + final int methodSecurityAnnotations = countSecurityAnnotations(method); + if (methodSecurityAnnotations > 1) { + throw new IllegalStateException(method.toString() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); } - /* - * Process annotations at the class level - */ - if (classSecurityAnnotations.size() > 1) { - throw new IllegalStateException(clazz.getName() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); + if (methodSecurityAnnotations == 1) { // method level annotations override class level ones + return toInterceptor(method); } - if (methodSecurityAnnotations.size() > 1) { - throw new IllegalStateException(method.toString() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); + final Class<?> constrainingClass = findConstrainingClass(resourceClass, method.getDeclaringClass()); + if (constrainingClass == null) { + return null; // nothing to do } - if (!methodSecurityAnnotations.isEmpty()) { // method level annotations override class level ones - return toInterceptor(method.getAnnotation(RolesAllowed.class), - method.isAnnotationPresent(DenyAll.class), - method.isAnnotationPresent(PermitAll.class)); + if (countSecurityAnnotations(constrainingClass) > 1) { + throw new IllegalStateException(constrainingClass.getName() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); } - return toInterceptor((RolesAllowed) clazz.getAnnotation(RolesAllowed.class), - clazz.isAnnotationPresent(DenyAll.class), - clazz.isAnnotationPresent(PermitAll.class)); + return toInterceptor(constrainingClass); } - private MPJWTSecurityAnnotationsInterceptor toInterceptor(final RolesAllowed rolesAllowed, - final boolean denyAll, - final boolean permitAll) { - final Set<String> roles = rolesAllowed == null ? null : new HashSet<>(Arrays.asList(rolesAllowed.value())); - return new MPJWTSecurityAnnotationsInterceptor(roles, denyAll, permitAll); - } + /* + * Security annotations are not @Inherited, so a class level annotation applies to the methods declared by + * that class. If the declaring class has none, the closest annotated class between the resource class and + * the declaring class applies, so a subclass can still secure the methods it inherits. + */ + private Class<?> findConstrainingClass(final Class<?> resourceClass, final Class<?> declaringClass) { + if (countSecurityAnnotations(declaringClass) > 0) { + return declaringClass; + } - private List<Class<? extends Annotation>[]> hasClassLevelAnnotations(final Class clazz, final Class<? extends Annotation>... annotationsToCheck) { - final List<Class<? extends Annotation>[]> list = new ArrayList<>(); - for (Class<? extends Annotation> annotationToCheck : annotationsToCheck) { - if (clazz.isAnnotationPresent(annotationToCheck)) { - list.add(annotationsToCheck); + for (Class<?> current = resourceClass; + current != null && current != declaringClass && current != Object.class; + current = current.getSuperclass()) { + + if (countSecurityAnnotations(current) > 0) { + return current; } } - return list; + + return null; + } + + private MPJWTSecurityAnnotationsInterceptor toInterceptor(final AnnotatedElement element) { + final RolesAllowed rolesAllowed = element.getAnnotation(RolesAllowed.class); + final Set<String> roles = rolesAllowed == null ? null : new HashSet<>(Arrays.asList(rolesAllowed.value())); + return new MPJWTSecurityAnnotationsInterceptor(roles, + element.isAnnotationPresent(DenyAll.class), + element.isAnnotationPresent(PermitAll.class)); } - private List<Class<? extends Annotation>[]> hasMethodLevelAnnotations(final Method method, final Class<? extends Annotation>... annotationsToCheck) { - final List<Class<? extends Annotation>[]> list = new ArrayList<>(); - for (Class<? extends Annotation> annotationToCheck : annotationsToCheck) { - if (method.isAnnotationPresent(annotationToCheck)) { - list.add(annotationsToCheck); + private int countSecurityAnnotations(final AnnotatedElement element) { + int count = 0; + for (final Class<? extends Annotation> annotation : SECURITY_ANNOTATIONS) { + if (element.isAnnotationPresent(annotation)) { + count++; } } - return list; + return count; } } \ No newline at end of file diff --git a/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeatureTest.java b/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeatureTest.java index b8e74c77fe..e11f0d8026 100644 --- a/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeatureTest.java +++ b/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeatureTest.java @@ -90,6 +90,38 @@ public class MPJWTSecurityAnnotationsInterceptorsFeatureTest { public static class NoAnnotationResource extends BaseResource { } + @RolesAllowed("base") + public static class SecuredBaseResource { + public String get() { + return "ok"; + } + + @PermitAll + public String open() { + return "ok"; + } + } + + public static class UnannotatedSecuredChildResource extends SecuredBaseResource { + public String own() { + return "ok"; + } + } + + @RolesAllowed("child") + public static class AnnotatedSecuredChildResource extends SecuredBaseResource { + public String own() { + return "ok"; + } + } + + @RolesAllowed("mid") + public static class MidResource extends BaseResource { + } + + public static class LeafResource extends MidResource { + } + public static class EmptyMethodRolesResource { @RolesAllowed({}) public String get() { @@ -117,6 +149,44 @@ public class MPJWTSecurityAnnotationsInterceptorsFeatureTest { assertAllowed(user, "user"); } + @Test + public void inheritedMethodKeepsDeclaringClassConstraint() throws Exception { + final MPJWTSecurityAnnotationsInterceptorsFeature feature = new MPJWTSecurityAnnotationsInterceptorsFeature(); + + final ContainerRequestFilter unannotated = configure(feature, UnannotatedSecuredChildResource.class, "get"); + assertForbidden(unannotated); + assertForbidden(unannotated, "user"); + assertAllowed(unannotated, "base"); + + final ContainerRequestFilter annotated = configure(feature, AnnotatedSecuredChildResource.class, "get"); + assertForbidden(annotated, "child"); + assertAllowed(annotated, "base"); + + assertAllowed(configure(feature, UnannotatedSecuredChildResource.class, "open")); + assertAllowed(configure(feature, AnnotatedSecuredChildResource.class, "open")); + } + + @Test + public void methodDeclaredInSubclassUsesSubclassConstraint() throws Exception { + final MPJWTSecurityAnnotationsInterceptorsFeature feature = new MPJWTSecurityAnnotationsInterceptorsFeature(); + + final ContainerRequestFilter annotated = configure(feature, AnnotatedSecuredChildResource.class, "own"); + assertForbidden(annotated, "base"); + assertAllowed(annotated, "child"); + + assertNull(configure(feature, UnannotatedSecuredChildResource.class, "own")); + } + + @Test + public void closestAnnotatedSuperclassSecuresUnannotatedDeclaringClass() throws Exception { + final MPJWTSecurityAnnotationsInterceptorsFeature feature = new MPJWTSecurityAnnotationsInterceptorsFeature(); + + final ContainerRequestFilter leaf = configure(feature, LeafResource.class, "get"); + assertForbidden(leaf); + assertForbidden(leaf, "admin"); + assertAllowed(leaf, "mid"); + } + @Test public void emptyRolesAllowedDeniesEveryone() throws Exception { final MPJWTSecurityAnnotationsInterceptorsFeature feature = new MPJWTSecurityAnnotationsInterceptorsFeature();
