This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new d82604c8ade Enforce every applicable @Scopes rule in OAuthScopesFilter 
(#3651)
d82604c8ade is described below

commit d82604c8ade55f8d58cb25ddd38b40ea4237d408
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Oct 30 04:36:30 2026 +0000

    Enforce every applicable @Scopes rule in OAuthScopesFilter (#3651)
    
    * Enforce every applicable @Scopes rule in OAuthScopesFilter
    
    * Fixes following review
---
 .../org/apache/cxf/common/util/ReflectionUtil.java |  93 +++++++++
 .../apache/cxf/common/util/ReflectionUtilTest.java |  92 +++++++++
 .../security/oauth2/filters/OAuthScopesFilter.java | 121 ++++++++---
 .../oauth2/filters/OAuthScopesFilterTest.java      | 229 ++++++++++++++++++++-
 4 files changed, 497 insertions(+), 38 deletions(-)

diff --git a/core/src/main/java/org/apache/cxf/common/util/ReflectionUtil.java 
b/core/src/main/java/org/apache/cxf/common/util/ReflectionUtil.java
index 92f89082aa1..26a58b11bf1 100644
--- a/core/src/main/java/org/apache/cxf/common/util/ReflectionUtil.java
+++ b/core/src/main/java/org/apache/cxf/common/util/ReflectionUtil.java
@@ -23,16 +23,28 @@ import java.beans.BeanInfo;
 import java.beans.PropertyDescriptor;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.apache.cxf.common.classloader.ClassLoaderUtils;
 
@@ -187,6 +199,87 @@ public final class ReflectionUtil {
         return sb.append(')').toString();
     }
 
+    /**
+     * Returns the methods of the superclasses and interfaces of a class which 
one of its public
+     * methods overrides or implements with different (erased) parameter 
types, because the
+     * overridden method's parameter types are type variables. For example, 
for update(String) of
+     * a class implementing Repository&lt;String&gt;, update(T) of Repository 
is returned.
+     */
+    public static List<Method> getGenericallyOverriddenMethods(final Class<?> 
cls, final Method method) {
+        Map<TypeVariable<?>, Type> typeArguments = new HashMap<>();
+        Set<Class<?>> supertypes = new LinkedHashSet<>();
+        collectTypeArguments(cls, typeArguments, supertypes);
+
+        List<Method> overridden = new ArrayList<>();
+        Class<?>[] paramTypes = method.getParameterTypes();
+        for (Class<?> supertype : supertypes) {
+            for (Method m : getDeclaredMethods(supertype)) {
+                if (m.isBridge() || Modifier.isStatic(m.getModifiers())
+                    || Modifier.isPrivate(m.getModifiers())
+                    || !m.getName().equals(method.getName())
+                    || m.getParameterCount() != paramTypes.length
+                    || Arrays.equals(m.getParameterTypes(), paramTypes)) {
+                    continue;
+                }
+                Type[] genericParamTypes = m.getGenericParameterTypes();
+                boolean matches = genericParamTypes.length == 
paramTypes.length;
+                for (int i = 0; matches && i < paramTypes.length; i++) {
+                    matches = resolveType(genericParamTypes[i], typeArguments) 
== paramTypes[i];
+                }
+                if (matches) {
+                    overridden.add(m);
+                }
+            }
+        }
+        return overridden;
+    }
+
+    private static void collectTypeArguments(Type type, Map<TypeVariable<?>, 
Type> typeArguments,
+                                             Set<Class<?>> supertypes) {
+        final Class<?> cls;
+        if (type instanceof ParameterizedType) {
+            ParameterizedType parameterizedType = (ParameterizedType)type;
+            cls = (Class<?>)parameterizedType.getRawType();
+            TypeVariable<?>[] typeVariables = cls.getTypeParameters();
+            Type[] actualTypes = parameterizedType.getActualTypeArguments();
+            for (int i = 0; i < typeVariables.length && i < 
actualTypes.length; i++) {
+                typeArguments.putIfAbsent(typeVariables[i], actualTypes[i]);
+            }
+        } else if (type instanceof Class) {
+            cls = (Class<?>)type;
+        } else {
+            return;
+        }
+        if (cls == Object.class || !supertypes.add(cls)) {
+            return;
+        }
+        collectTypeArguments(cls.getGenericSuperclass(), typeArguments, 
supertypes);
+        for (Type interfaceType : cls.getGenericInterfaces()) {
+            collectTypeArguments(interfaceType, typeArguments, supertypes);
+        }
+    }
+
+    private static Class<?> resolveType(Type type, Map<TypeVariable<?>, Type> 
typeArguments) {
+        if (type instanceof Class) {
+            return (Class<?>)type;
+        } else if (type instanceof ParameterizedType) {
+            return (Class<?>)((ParameterizedType)type).getRawType();
+        } else if (type instanceof GenericArrayType) {
+            Class<?> componentType =
+                
resolveType(((GenericArrayType)type).getGenericComponentType(), typeArguments);
+            return Array.newInstance(componentType, 0).getClass();
+        } else if (type instanceof WildcardType) {
+            return resolveType(((WildcardType)type).getUpperBounds()[0], 
typeArguments);
+        } else if (type instanceof TypeVariable) {
+            // A type variable which is not bound by the class's hierarchy 
resolves to its bound
+            Type actualType = typeArguments.get(type);
+            return actualType != null && actualType != type
+                ? resolveType(actualType, typeArguments)
+                : resolveType(((TypeVariable<?>)type).getBounds()[0], 
typeArguments);
+        }
+        return Object.class;
+    }
+
     public static Field[] getDeclaredFields(final Class<?> cls) {
         return AccessController.doPrivileged(new PrivilegedAction<Field[]>() {
             public Field[] run() {
diff --git 
a/core/src/test/java/org/apache/cxf/common/util/ReflectionUtilTest.java 
b/core/src/test/java/org/apache/cxf/common/util/ReflectionUtilTest.java
new file mode 100644
index 00000000000..d4f094253b0
--- /dev/null
+++ b/core/src/test/java/org/apache/cxf/common/util/ReflectionUtilTest.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.common.util;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ReflectionUtilTest {
+
+    @Test
+    public void testGetMethodSignature() throws Exception {
+        assertEquals("update(java.lang.String,int)",
+            ReflectionUtil.getMethodSignature(Impl.class.getMethod("update", 
String.class, int.class)));
+        assertEquals("update()", 
ReflectionUtil.getMethodSignature(Impl.class.getMethod("update")));
+    }
+
+    @Test
+    public void testGetGenericallyOverriddenMethods() throws Exception {
+        List<Method> overridden = 
ReflectionUtil.getGenericallyOverriddenMethods(Impl.class,
+            Impl.class.getMethod("update", String.class));
+        
assertEquals(Collections.singletonList(Repository.class.getMethod("update", 
Object.class)),
+                     overridden);
+    }
+
+    @Test
+    public void testGetGenericallyOverriddenMethodsIgnoresUnrelatedOverloads() 
throws Exception {
+        assertTrue(ReflectionUtil.getGenericallyOverriddenMethods(Impl.class,
+            Impl.class.getMethod("update", Integer.class)).isEmpty());
+        assertTrue(ReflectionUtil.getGenericallyOverriddenMethods(Impl.class,
+            Impl.class.getMethod("update")).isEmpty());
+    }
+
+    @Test
+    public void 
testGetGenericallyOverriddenMethodsWithInheritedImplementation() throws 
Exception {
+        List<Method> overridden = 
ReflectionUtil.getGenericallyOverriddenMethods(InheritingImpl.class,
+            InheritingImpl.class.getMethod("update", String.class));
+        
assertEquals(Collections.singletonList(Repository.class.getMethod("update", 
Object.class)),
+                     overridden);
+    }
+
+    public interface Repository<T> {
+        void update(T value);
+    }
+
+    public abstract static class AbstractRepository<T> implements 
Repository<T> { // NOPMD
+    }
+
+    public static class Impl extends AbstractRepository<String> {
+        @Override
+        public void update(String value) {
+        }
+
+        public void update(Integer value) {
+        }
+
+        public void update(String value, int count) {
+        }
+
+        public void update() {
+        }
+    }
+
+    public static class StringUpdater {
+        public void update(String value) {
+        }
+    }
+
+    public static class InheritingImpl extends StringUpdater implements 
Repository<String> {
+    }
+}
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilter.java
 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilter.java
index a43cb02d6c1..c3c50bf4f0e 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilter.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilter.java
@@ -25,6 +25,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -58,53 +59,61 @@ public class OAuthScopesFilter implements 
ContainerRequestFilter {
     // Scopes configured explicitly via setScopesMap/setScopesStringMap, keyed 
by method name
     private Map<String, List<String>> scopesMap = new HashMap<>();
     private Map<String, Boolean> scopesMatchAllMap = new HashMap<>();
-    // Scopes discovered from @Scopes annotations. Method-level and 
class-level annotations
-    // are keyed by method signature so that overloaded methods do not share a 
rule and an
-    // annotation on an overriding method is not replaced by one from a 
superclass or interface
-    private final Map<String, Scopes> methodScopesBySignature = new 
HashMap<>();
-    private final Map<String, Scopes> classScopesBySignature = new HashMap<>();
-    // All the annotation-derived rules for a given method name, enforced 
together when the
-    // invoked method's signature is not recorded (e.g. it implements a 
generic interface method)
-    private final Map<String, Set<Scopes>> annotationScopesByName = new 
HashMap<>();
+    // The method-level and class-level @Scopes declarations which apply to 
each method signature
+    // in the secured object's hierarchy. Keying by signature means that 
overloaded methods do
+    // not share a rule
+    private final Map<String, List<ScopesDeclaration>> declarationsBySignature 
= new HashMap<>();
+    // The erased signatures a method overrides with a different signature, 
e.g. update(Object)
+    // for update(String) implementing update(T) of Repository<String>
+    private final Map<String, Set<String>> overriddenSignatures = new 
HashMap<>();
+    private final Map<String, Set<String>> signaturesByName = new HashMap<>();
+    private final Set<Class<?>> securedClasses = new HashSet<>();
+    private boolean annotationScopesFound;
     private Set<String> confidentialClientMethods = new HashSet<>();
 
     public void setSecuredObject(Object object) {
         Class<?> cls = ClassHelper.getRealClass(object);
         checkSecureClass(cls);
-        if (annotationScopesByName.isEmpty()) {
+        if (!annotationScopesFound) {
             LOG.warning("The scopes map is empty");
         } else if (LOG.isLoggable(Level.FINE)) {
-            for (Map.Entry<String, Set<Scopes>> entry : 
annotationScopesByName.entrySet()) {
-                for (Scopes scopes : entry.getValue()) {
-                    LOG.fine("Method: " + entry.getKey() + ", scopes: " + 
Arrays.asList(scopes.value()));
+            for (String signature : declarationsBySignature.keySet()) {
+                for (Scopes scopes : getAnnotationScopes(signature)) {
+                    LOG.fine("Method: " + signature + ", scopes: " + 
Arrays.asList(scopes.value()));
                 }
             }
         }
     }
 
     protected void checkSecureClass(Class<?> cls) {
-        if (cls == null || cls == Object.class) {
+        if (cls == null || cls == Object.class || !securedClasses.add(cls)) {
             return;
         }
+        // The class-level @Scopes may be inherited from a superclass (@Scopes 
is @Inherited)
         Scopes classScopes = cls.getAnnotation(Scopes.class);
+        Class<?> classScopesDeclarer = getScopesDeclarer(cls);
         ConfidentialClient classConfClient = 
cls.getAnnotation(ConfidentialClient.class);
         for (Method m : cls.getMethods()) {
             if (SKIP_METHODS.contains(m.getName())) {
                 continue;
             }
-            // Subclasses are scanned before their superclasses and 
interfaces, so the most
-            // specific declaration of a given signature is recorded first and 
kept
             String signature = ReflectionUtil.getMethodSignature(m);
             Scopes methodScopes = m.getAnnotation(Scopes.class);
-            // Each overload has its own signature and so its own rule. The 
rules are also
-            // collected by name, but that is only used as a fallback when the 
invoked
-            // method's signature was not recorded, and then all of them have 
to be met
+            List<ScopesDeclaration> declarations =
+                declarationsBySignature.computeIfAbsent(signature, k -> new 
ArrayList<>());
             if (methodScopes != null) {
-                methodScopesBySignature.putIfAbsent(signature, methodScopes);
-                annotationScopesByName.computeIfAbsent(m.getName(), k -> new 
HashSet<>()).add(methodScopes);
-            } else if (classScopes != null) {
-                classScopesBySignature.putIfAbsent(signature, classScopes);
-                annotationScopesByName.computeIfAbsent(m.getName(), k -> new 
HashSet<>()).add(classScopes);
+                declarations.add(new ScopesDeclaration(m.getDeclaringClass(), 
methodScopes, true));
+            }
+            if (classScopes != null) {
+                declarations.add(new ScopesDeclaration(classScopesDeclarer, 
classScopes, false));
+            }
+            signaturesByName.computeIfAbsent(m.getName(), k -> new 
HashSet<>()).add(signature);
+            annotationScopesFound |= methodScopes != null || classScopes != 
null;
+            if (!m.isBridge()) {
+                for (Method overridden : 
ReflectionUtil.getGenericallyOverriddenMethods(cls, m)) {
+                    overriddenSignatures.computeIfAbsent(signature, k -> new 
HashSet<>())
+                        .add(ReflectionUtil.getMethodSignature(overridden));
+                }
             }
 
             ConfidentialClient mConfClient = 
m.getAnnotation(ConfidentialClient.class);
@@ -161,19 +170,56 @@ public class OAuthScopesFilter implements 
ContainerRequestFilter {
         }
     }
 
+    private static Class<?> getScopesDeclarer(Class<?> cls) {
+        Class<?> declarer = cls;
+        while (declarer != null && 
declarer.getDeclaredAnnotation(Scopes.class) == null) {
+            declarer = declarer.getSuperclass();
+        }
+        return declarer == null ? cls : declarer;
+    }
+
     private List<Scopes> getAnnotationScopes(Method m) {
         String signature = ReflectionUtil.getMethodSignature(m);
-        // A method-level annotation anywhere in the hierarchy takes 
precedence over a
-        // class-level one, otherwise an inherited class-level @Scopes would 
replace it
-        Scopes scopes = methodScopesBySignature.get(signature);
-        if (scopes == null) {
-            scopes = classScopesBySignature.get(signature);
+        Set<String> signatures = new HashSet<>();
+        if (declarationsBySignature.containsKey(signature)) {
+            signatures.add(signature);
+            signatures.addAll(overriddenSignatures.getOrDefault(signature, 
Collections.emptySet()));
+        } else {
+            // The method is not one of the secured object's methods, so every 
rule
+            // recorded for its name applies
+            signatures.addAll(signaturesByName.getOrDefault(m.getName(), 
Collections.emptySet()));
+        }
+        Set<Scopes> scopes = new LinkedHashSet<>();
+        for (String s : signatures) {
+            scopes.addAll(getAnnotationScopes(s));
+        }
+        return new ArrayList<>(scopes);
+    }
+
+    private List<Scopes> getAnnotationScopes(String signature) {
+        // Every @Scopes which applies to the method has to be met, from any 
class or interface
+        // in the hierarchy. A class-level @Scopes is replaced by a 
method-level @Scopes declared on
+        // the same class or interface, or on a subclass of it
+        List<ScopesDeclaration> declarations =
+            declarationsBySignature.getOrDefault(signature, 
Collections.emptyList());
+        Set<Scopes> scopes = new LinkedHashSet<>();
+        for (ScopesDeclaration declaration : declarations) {
+            if (declaration.methodLevel || 
!isReplacedByMethodScopes(declaration, declarations)) {
+                scopes.add(declaration.scopes);
+            }
         }
-        if (scopes != null) {
-            return Collections.singletonList(scopes);
+        return new ArrayList<>(scopes);
+    }
+
+    private static boolean isReplacedByMethodScopes(ScopesDeclaration 
classDeclaration,
+                                                    List<ScopesDeclaration> 
declarations) {
+        for (ScopesDeclaration declaration : declarations) {
+            if (declaration.methodLevel
+                && 
classDeclaration.declaringClass.isAssignableFrom(declaration.declaringClass)) {
+                return true;
+            }
         }
-        Set<Scopes> scopesForName = annotationScopesByName.get(m.getName());
-        return scopesForName == null ? Collections.emptyList() : new 
ArrayList<>(scopesForName);
+        return false;
     }
 
     private static boolean isAllowed(List<String> requestScopes, List<String> 
methodScopes,
@@ -215,5 +261,16 @@ public class OAuthScopesFilter implements 
ContainerRequestFilter {
         this.confidentialClientMethods = confidentialClientMethods;
     }
 
+    private static final class ScopesDeclaration {
+        // The class or interface the annotation is declared on
+        private final Class<?> declaringClass;
+        private final Scopes scopes;
+        private final boolean methodLevel;
 
+        ScopesDeclaration(Class<?> declaringClass, Scopes scopes, boolean 
methodLevel) {
+            this.declaringClass = declaringClass;
+            this.scopes = scopes;
+            this.methodLevel = methodLevel;
+        }
+    }
 }
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilterTest.java
 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilterTest.java
index ca125bfef26..c8d165e7f10 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilterTest.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthScopesFilterTest.java
@@ -63,21 +63,38 @@ public class OAuthScopesFilterTest {
     }
 
     @Test
-    public void testOverridingMethodScopesTakePrecedenceOverSuperclass() 
throws Exception {
+    public void testOverridingMethodScopesApplyTogetherWithSuperclassScopes() 
throws Exception {
         Method method = OverridingService.class.getMethod("operation");
 
-        assertAllowed(new OverridingService(), method, "admin");
+        assertAllowed(new OverridingService(), method, "read", "admin");
         assertForbidden(new OverridingService(), method, "read");
+        assertForbidden(new OverridingService(), method, "admin");
     }
 
     @Test
-    public void 
testOverridingMethodScopesTakePrecedenceOverInterfaceClassScopes() throws 
Exception {
+    public void testOverridingMethodCannotWeakenSuperclassScopes() throws 
Exception {
+        Method method = ReadOverridingService.class.getMethod("operation");
+
+        assertAllowed(new ReadOverridingService(), method, "read", "admin");
+        assertForbidden(new ReadOverridingService(), method, "read");
+    }
+
+    @Test
+    public void testOverridingMethodScopesReplaceInterfaceClassScopes() throws 
Exception {
         Method method = 
InterfaceOverridingService.class.getMethod("operation");
 
         assertAllowed(new InterfaceOverridingService(), method, "admin");
         assertForbidden(new InterfaceOverridingService(), method, "read");
     }
 
+    @Test
+    public void testOverridingMethodScopesReplaceSuperclassClassScopes() 
throws Exception {
+        Method method = OverridingSubResource.class.getMethod("operation");
+
+        assertAllowed(new OverridingSubResource(), method, "admin");
+        assertForbidden(new OverridingSubResource(), method, "read");
+    }
+
     @Test
     public void testMatchAllIsResolvedPerSignature() throws Exception {
         Method matchAllMethod = MatchAllService.class.getMethod("operation", 
String.class);
@@ -127,13 +144,95 @@ public class OAuthScopesFilterTest {
     public void testGenericInterfaceMethodScopesAreEnforced() throws Exception 
{
         Method updateMethod = StringRepository.class.getMethod("update", 
String.class);
 
-        // The erased interface signature does not match, so every rule for 
the name applies
-        assertAllowed(new StringRepository(), updateMethod, "admin", "read");
+        // update(String) overrides the erased update(Object), but not the 
update() overload
+        assertAllowed(new StringRepository(), updateMethod, "admin");
         assertForbidden(new StringRepository(), updateMethod, "read");
-        assertForbidden(new StringRepository(), updateMethod, "admin");
 
         Method readMethod = StringRepository.class.getMethod("update");
         assertAllowed(new StringRepository(), readMethod, "read");
+        assertForbidden(new StringRepository(), readMethod, "admin");
+    }
+
+    @Test
+    public void testGenericInterfaceMethodScopesApplyTogetherWithClassScopes() 
throws Exception {
+        Method method = ClassScopesStringRepository.class.getMethod("update", 
String.class);
+
+        assertAllowed(new ClassScopesStringRepository(), method, "user", 
"admin");
+        assertForbidden(new ClassScopesStringRepository(), method, "user");
+        assertForbidden(new ClassScopesStringRepository(), method, "admin");
+    }
+
+    @Test
+    public void testSubclassScopesApplyTogetherWithSuperclassMethodScopes() 
throws Exception {
+        Method method = AdminSubService.class.getMethod("operation");
+
+        assertAllowed(new AdminSubService(), method, "read", "admin");
+        assertForbidden(new AdminSubService(), method, "read");
+        assertForbidden(new AdminSubService(), method, "admin");
+    }
+
+    @Test
+    public void 
testGenericInterfaceMethodScopesApplyToInheritedImplementation() throws 
Exception {
+        Method method = InheritingStringRepository.class.getMethod("update", 
String.class);
+
+        assertAllowed(new InheritingStringRepository(), method, "admin");
+        assertForbidden(new InheritingStringRepository(), method, "read");
+    }
+
+    @Test
+    public void testMethodScopesFromUnrelatedSupertypesApplyTogether() throws 
Exception {
+        Method method = AdminApiReadService.class.getMethod("operation");
+
+        assertAllowed(new AdminApiReadService(), method, "read", "admin");
+        assertForbidden(new AdminApiReadService(), method, "read");
+        assertForbidden(new AdminApiReadService(), method, "admin");
+    }
+
+    @Test
+    public void testMethodScopesReplaceInheritedClassScopes() throws Exception 
{
+        Method method = 
InheritingAdminService.class.getMethod("adminOperation");
+        assertAllowed(new InheritingAdminService(), method, "admin");
+        assertForbidden(new InheritingAdminService(), method, "read");
+
+        Method redeclaredMethod = 
RedeclaringAdminService.class.getMethod("adminOperation");
+        assertAllowed(new RedeclaringAdminService(), redeclaredMethod, 
"admin");
+        assertForbidden(new RedeclaringAdminService(), redeclaredMethod, 
"read");
+    }
+
+    @Test
+    public void testUnrelatedOverloadIsNotLinkedToGenericInterfaceMethod() 
throws Exception {
+        Method integerMethod = 
OverloadingStringRepository.class.getMethod("update", Integer.class);
+        assertAllowed(new OverloadingStringRepository(), integerMethod, 
"read");
+        assertForbidden(new OverloadingStringRepository(), integerMethod, 
"admin");
+
+        Method stringMethod = 
OverloadingStringRepository.class.getMethod("update", String.class);
+        assertAllowed(new OverloadingStringRepository(), stringMethod, 
"admin");
+        assertForbidden(new OverloadingStringRepository(), stringMethod, 
"read");
+    }
+
+    @Test
+    public void testGenericInterfaceMethodScopesApplyThroughTypeVariables() 
throws Exception {
+        Method method = TypedStringRepository.class.getMethod("update", 
String.class);
+
+        assertAllowed(new TypedStringRepository(), method, "admin");
+        assertForbidden(new TypedStringRepository(), method, "read");
+    }
+
+    @Test
+    public void testUnannotatedOverloadIsNotSecuredByOtherOverloads() throws 
Exception {
+        Method method = UnannotatedOverloadService.class.getMethod("delete");
+
+        assertAllowed(new UnannotatedOverloadService(), method, "user");
+        assertForbidden(new UnannotatedOverloadService(),
+                        UnannotatedOverloadService.class.getMethod("delete", 
String.class), "user");
+    }
+
+    @Test
+    public void testInheritedClassScopesApplyToSubclassMethods() throws 
Exception {
+        Method method = 
InheritedClassScopesService.class.getMethod("subclassOperation");
+
+        assertAllowed(new InheritedClassScopesService(), method, "read");
+        assertForbidden(new InheritedClassScopesService(), method, "write");
     }
 
     @Test
@@ -213,6 +312,19 @@ public class OAuthScopesFilterTest {
         }
     }
 
+    public static class AdminBaseService {
+        @Scopes("admin")
+        public void operation() {
+        }
+    }
+
+    public static class ReadOverridingService extends AdminBaseService {
+        @Override
+        @Scopes("read")
+        public void operation() {
+        }
+    }
+
     public static class UnannotatedOverridingService extends BaseService {
         @Override
         public void operation() {
@@ -248,6 +360,9 @@ public class OAuthScopesFilterTest {
         @Override
         public void operation() {
         }
+
+        public void subclassOperation() {
+        }
     }
 
     public interface Repository<T> {
@@ -268,6 +383,108 @@ public class OAuthScopesFilterTest {
         }
     }
 
+    @Scopes("user")
+    public static class ClassScopesStringRepository implements 
Repository<String> {
+        @Override
+        public void update(String value) {
+        }
+
+        @Override
+        public void update() {
+        }
+    }
+
+    @Scopes("admin")
+    public static class AdminSubService extends BaseService {
+        @Override
+        public void operation() {
+        }
+    }
+
+    public static class StringUpdater {
+        public void update(String value) {
+        }
+
+        public void update() {
+        }
+    }
+
+    public static class InheritingStringRepository extends StringUpdater 
implements Repository<String> {
+    }
+
+    public interface AdminApi {
+        @Scopes("admin")
+        void operation();
+    }
+
+    public static class AdminApiReadService extends BaseService implements 
AdminApi {
+        @Override
+        public void operation() {
+        }
+    }
+
+    @Scopes("read")
+    public static class ClassScopesBaseResource {
+        public void operation() {
+        }
+    }
+
+    public static class OverridingSubResource extends ClassScopesBaseResource {
+        @Override
+        @Scopes("admin")
+        public void operation() {
+        }
+    }
+
+    public static class InheritingAdminService extends 
InheritedClassScopesBaseService {
+        @Scopes("admin")
+        public void adminOperation() {
+        }
+    }
+
+    @Scopes("read")
+    public static class RedeclaringAdminService extends 
InheritedClassScopesBaseService {
+        @Scopes("admin")
+        public void adminOperation() {
+        }
+    }
+
+    public static class OverloadingStringRepository implements 
Repository<String> {
+        @Override
+        public void update(String value) {
+        }
+
+        @Override
+        public void update() {
+        }
+
+        @Scopes("read")
+        public void update(Integer value) {
+        }
+    }
+
+    public abstract static class AbstractRepository<T> implements 
Repository<T> { //NOPMD
+    }
+
+    public static class TypedStringRepository extends 
AbstractRepository<String> {
+        @Override
+        public void update(String value) {
+        }
+
+        @Override
+        public void update() {
+        }
+    }
+
+    public static class UnannotatedOverloadService {
+        @Scopes("admin")
+        public void delete(String id) {
+        }
+
+        public void delete() {
+        }
+    }
+
     public static class UnsecuredService {
         public void operation(String value) {
         }

Reply via email to