============================================================================== Demo 5 -- filterObject: mutability, container types, and the silent no-op ============================================================================== SLF4J(W): No SLF4J providers were found. SLF4J(W): Defaulting to no-operation (NOP) logger implementation SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details. @PreFilter needs a MUTABLE argument -- and does not tell you when it is not --------------------------------------------------------------------------- method body saw: [Account[1,alice,100], Account[3,alice,300]] new ArrayList<>(..) ALLOWED -> (void) method body saw: [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] List.of(..) (immutable) ALLOWED -> (void) method body saw: [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] List.copyOf(..) (immutable) ALLOWED -> (void) method body saw: [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] Arrays.asList(..) ALLOWED -> (void) method body saw: [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] Collections.unmodifiableList(..) ALLOWED -> (void) method body saw: [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] stream().toList() (unmodifiable since 16) ALLOWED -> (void) method body saw: [Account[1,alice,100], Account[3,alice,300]] stream().collect(toList()) (ArrayList) ALLOWED -> (void) Account[] (arrays rejected outright) DENIED -> IllegalStateException: Pre-filtering on array types is not supported. Using a Collection will solve this problem. Read the second and third lines again: bob's account reached the method body. @PreFilter filters by CLEARING the caller's collection and adding the survivors back. On an immutable list that throws, and DefaultMethodSecurityExpressionHandler.filterCollection catches the UnsupportedOperationException and returns a fresh list instead -- which PreFilterAuthorizationMethodInterceptor.invoke then discards, because it ignores filter()'s return value entirely. No exception, no WARN, no 403. The only trace it leaves (same call, logger at TRACE) ----------------------------------------------------- method body saw: [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] List.of(..) with TRACE on ALLOWED -> (void) What @PostFilter accepts as a return type ----------------------------------------- List ALLOWED -> [Account[1,alice,100], Account[3,alice,300]] Account[] ALLOWED -> [Account[1,alice,100], Account[3,alice,300]] Stream (collected here) ALLOWED -> [alice, alice] Map ALLOWED -> {acct-1=Account[1,alice,100], acct-3=Account[3,alice,300]} Optional (alice's) DENIED -> IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was Optional[Account[1,alice,100]] Optional (bob's) DENIED -> IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was Optional[Account[2,bob,200]] List.of(..) (immutable return) ALLOWED -> [Account[1,alice,100], Account[3,alice,300]] Ledger (a type Spring Security does not know) DENIED -> IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was Ledger[Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]] Page (real Spring Data PageImpl) DENIED -> IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was Page 1 of 1 containing com.ankurm.methodsec.Account instances Identity: does @PostFilter hand back the same object? ----------------------------------------------------- returned == the list the method returned : true the method's own list, after filtering : [Account[1,alice,100], Account[3,alice,300]] @PostFilter mutates the returned collection in place and hands the same reference back. If that collection is a cached or shared instance, you have just deleted rows from it for every future caller.