Split into per-article modules and add the method-security module
Moves the existing virtual-thread/context-propagation project into context-propagation/ and adds method-security/ for the Spring Security 7 method-security article: nine runnable demos, fourteen assertions, and every transcript the article quotes, regenerated by scripts/run-all.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
This commit is contained in:
50
method-security/docs/output/demo1.txt
Normal file
50
method-security/docs/output/demo1.txt
Normal file
@@ -0,0 +1,50 @@
|
||||
==============================================================================
|
||||
Demo 1 -- the four pre/post annotations, @Secured and JSR-250, all switched on
|
||||
==============================================================================
|
||||
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.
|
||||
|
||||
as alice (ROLE_USER)
|
||||
--------------------
|
||||
@PreAuthorize hasRole('ADMIN') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@PreAuthorize #owner == authentication.name ALLOWED -> [Account[1,alice,100], Account[3,alice,300]]
|
||||
@PreAuthorize #owner == authentication.name DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@PostAuthorize returnObject.owner == ...name ALLOWED -> Account[1,alice,100]
|
||||
@PostAuthorize returnObject.owner == ...name DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@PostFilter filterObject.owner == ...name ALLOWED -> [Account[1,alice,100], Account[3,alice,300]]
|
||||
@Secured("ROLE_ADMIN") DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@RolesAllowed("ADMIN") DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@PermitAll ALLOWED -> open payload
|
||||
@DenyAll DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
as root (ROLE_ADMIN, ROLE_USER)
|
||||
-------------------------------
|
||||
@PreAuthorize hasRole('ADMIN') ALLOWED -> the admin console
|
||||
@Secured("ROLE_ADMIN") ALLOWED -> secured payload
|
||||
@RolesAllowed("ADMIN") ALLOWED -> jsr250 payload
|
||||
@PostFilter filterObject.owner == ...name ALLOWED -> []
|
||||
|
||||
with no Authentication at all (SecurityContextHolder cleared)
|
||||
-------------------------------------------------------------
|
||||
@PreAuthorize hasRole('ADMIN') DENIED -> AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
|
||||
@PermitAll ALLOWED -> open payload
|
||||
|
||||
@PreFilter -- filtering the ARGUMENT, as alice
|
||||
----------------------------------------------
|
||||
caller's list before the call : [Account[1,alice,100], Account[2,bob,200], Account[3,alice,300]]
|
||||
method body saw : [Account[1,alice,150], Account[3,alice,350]]
|
||||
caller's list after the call : [Account[1,alice,150], Account[3,alice,350]]
|
||||
|
||||
@PreFilter did not hand the method a copy. It removed bob's account from
|
||||
the caller's own list, in place, before the method body ever ran. That is
|
||||
why Demo 5's immutable List.of(..) blows up.
|
||||
|
||||
@PreFilter on a method with more than one argument
|
||||
--------------------------------------------------
|
||||
no filterTarget, 2 args DENIED -> IllegalStateException: Unable to determine the method argument for filtering. Specify the filter target.
|
||||
method body saw : [Account[4,alice,60]]
|
||||
filterTarget = "accounts" ALLOWED -> (void)
|
||||
|
||||
This one is loud, not silent -- but it only fires when the method is
|
||||
actually called, so a rarely-exercised path can ship broken.
|
||||
27
method-security/docs/output/demo2.txt
Normal file
27
method-security/docs/output/demo2.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
==============================================================================
|
||||
Demo 2 -- self-invocation: the annotation is there, the check is not
|
||||
==============================================================================
|
||||
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.
|
||||
|
||||
alice has ROLE_USER only. adminReport() requires ROLE_ADMIN.
|
||||
------------------------------------------------------------
|
||||
reports.adminReport() (via proxy) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
reports.userEntryPoint() (this.adminReport()) ALLOWED -> TOP SECRET REVENUE NUMBERS
|
||||
|
||||
Is the annotation actually there? (reflection on the target class)
|
||||
------------------------------------------------------------------
|
||||
ReportService.adminReport() @PreAuthorize -> @org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')")
|
||||
bean is an AOP proxy -> true
|
||||
proxy class -> com.ankurm.methodsec.Demo2SelfInvocation$ReportService$$SpringCGLIB$$0
|
||||
target class -> com.ankurm.methodsec.Demo2SelfInvocation$ReportService
|
||||
|
||||
The annotation is present, the bean IS proxied, and the call was still
|
||||
not checked. The proxy only sees calls that arrive from outside.
|
||||
|
||||
Three ways to make the inner call go through the proxy
|
||||
------------------------------------------------------
|
||||
self-injection (ObjectProvider) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
AopContext.currentProxy() DENIED -> AuthorizationDeniedException: Access Denied
|
||||
call a different bean (collaborator) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
37
method-security/docs/output/demo3.txt
Normal file
37
method-security/docs/output/demo3.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
==============================================================================
|
||||
Demo 3 -- @PreAuthorize on methods the proxy cannot override
|
||||
==============================================================================
|
||||
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.
|
||||
|
||||
alice has ROLE_USER. Every method below says hasRole('ADMIN').
|
||||
--------------------------------------------------------------
|
||||
public (overridable) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
public final (NOT overridable) ALLOWED -> final payload
|
||||
static (NOT overridable) ALLOWED -> static payload
|
||||
package-private (overridable, same package) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
private, reached via a public wrapper ALLOWED -> private payload
|
||||
|
||||
What the proxy actually overrode
|
||||
--------------------------------
|
||||
publicAdminOnly declared final=false overridden by proxy=true
|
||||
finalAdminOnly declared final=true overridden by proxy=false
|
||||
packagePrivateAdminOnly declared final=false overridden by proxy=true
|
||||
proxy class -> com.ankurm.methodsec.Demo3NonProxyable$Vault$$SpringCGLIB$$0
|
||||
|
||||
A JDK dynamic proxy only advises methods that are ON the interface
|
||||
------------------------------------------------------------------
|
||||
proxy is a JDK proxy -> true
|
||||
proxied interfaces -> [interface com.ankurm.methodsec.Demo3NonProxyable$LedgerOperations]
|
||||
onTheInterface() (advised) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
notOnTheInterface() is public and annotated, but the JDK proxy does not
|
||||
implement it at all -- a caller cannot even reach it without casting to
|
||||
the implementation class, and that cast throws ClassCastException.
|
||||
cast proxy to Ledger impl class DENIED -> ClassCastException: class jdk.proxy2.$Proxy18 cannot be cast to class com.ankurm.methodsec.Demo3NonProxyable$Ledger (jdk.proxy2.$Proxy18 is in module jdk.proxy2 of loader 'app'; com.ankurm.methodsec.Demo3NonProxyable$Ledger is in unnamed module of loader 'app')
|
||||
|
||||
A final CLASS is the loud one
|
||||
-----------------------------
|
||||
startup FAILED -> BeanCreationException
|
||||
root cause -> java.lang.IllegalArgumentException
|
||||
message -> Cannot subclass final class com.ankurm.methodsec.Demo3NonProxyable$SealedVault
|
||||
69
method-security/docs/output/demo4.txt
Normal file
69
method-security/docs/output/demo4.txt
Normal file
@@ -0,0 +1,69 @@
|
||||
==============================================================================
|
||||
Demo 4 -- what you can actually write inside @PreAuthorize / @PostAuthorize
|
||||
==============================================================================
|
||||
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.
|
||||
|
||||
alice: ROLE_USER, ROLE_AUDITOR, plus the authority 'report:read'
|
||||
----------------------------------------------------------------
|
||||
permitAll ALLOWED -> ok
|
||||
denyAll DENIED -> AuthorizationDeniedException: Access Denied
|
||||
isAuthenticated() ALLOWED -> ok
|
||||
isAnonymous() DENIED -> AuthorizationDeniedException: Access Denied
|
||||
isFullyAuthenticated() ALLOWED -> ok
|
||||
isRememberMe() DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasRole('ADMIN') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasAnyRole('ADMIN','AUDITOR') ALLOWED -> ok
|
||||
hasAllRoles('USER','AUDITOR') ALLOWED -> ok
|
||||
hasAuthority('report:read') ALLOWED -> ok
|
||||
hasAnyAuthority('report:read','x') ALLOWED -> ok
|
||||
hasAllAuthorities('report:read','x') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
authentication.name == 'alice' ALLOWED -> ok
|
||||
principal == 'alice' ALLOWED -> ok
|
||||
|
||||
root: ROLE_ADMIN only (RoleHierarchy says ADMIN > USER > GUEST)
|
||||
---------------------------------------------------------------
|
||||
permitAll ALLOWED -> ok
|
||||
denyAll DENIED -> AuthorizationDeniedException: Access Denied
|
||||
isAuthenticated() ALLOWED -> ok
|
||||
isAnonymous() DENIED -> AuthorizationDeniedException: Access Denied
|
||||
isFullyAuthenticated() ALLOWED -> ok
|
||||
isRememberMe() DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasRole('ADMIN') ALLOWED -> ok
|
||||
hasAnyRole('ADMIN','AUDITOR') ALLOWED -> ok
|
||||
hasAllRoles('USER','AUDITOR') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasAuthority('report:read') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasAnyAuthority('report:read','x') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasAllAuthorities('report:read','x') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
authentication.name == 'alice' DENIED -> AuthorizationDeniedException: Access Denied
|
||||
principal == 'alice' DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
Role prefix and hierarchy
|
||||
-------------------------
|
||||
hasRole('USER') -> ROLE_USER ALLOWED -> ok
|
||||
hasAuthority('USER') -> literal 'USER' DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasAuthority('ROLE_USER') ALLOWED -> ok
|
||||
root hasRole('GUEST') via RoleHierarchy ALLOWED -> ok
|
||||
|
||||
Method arguments, the return value, and bean references
|
||||
-------------------------------------------------------
|
||||
#owner == authentication.name ("alice") ALLOWED -> ok
|
||||
#owner == authentication.name ("bob") DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@P("o") alias, #o == ...name ("alice") ALLOWED -> ok
|
||||
#root.this (the target object) ALLOWED -> ok
|
||||
#root.args[0] -- no such property DENIED -> IllegalArgumentException: Failed to evaluate expression '#root.args[0] == authentication.name' [cause: SpelEvaluationException: EL1008E: Property or field 'args' cannot be found on object of type 'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot' - maybe not public or not valid?]
|
||||
@policy.canRead(authentication, #id) id=1 ALLOWED -> ok
|
||||
@policy.canRead(authentication, #id) id=9 DENIED -> AuthorizationDeniedException: Access Denied
|
||||
hasPermission(#id, 'account', 'read') id=1 ALLOWED -> ok
|
||||
hasPermission(#id, 'account', 'read') id=9 DENIED -> AuthorizationDeniedException: Access Denied
|
||||
T(java.time.LocalDate) type reference ALLOWED -> ok
|
||||
@PostAuthorize returnObject.owner == ...name ALLOWED -> Account[1,alice,100]
|
||||
@PostAuthorize returnObject.owner == ...name DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
The literal constants on SecurityExpressionRoot
|
||||
-----------------------------------------------
|
||||
permitAll / denyAll exist as BOTH a boolean field and a no-arg method,
|
||||
and read/write/create/delete/admin are String constants meant for
|
||||
hasPermission(..) -- e.g. hasPermission(#id, 'account', read).
|
||||
hasPermission(#id, 'account', read) id=1 ALLOWED -> ok
|
||||
58
method-security/docs/output/demo5.txt
Normal file
58
method-security/docs/output/demo5.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
==============================================================================
|
||||
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<Account> ALLOWED -> [Account[1,alice,100], Account[3,alice,300]]
|
||||
Account[] ALLOWED -> [Account[1,alice,100], Account[3,alice,300]]
|
||||
Stream<Account> (collected here) ALLOWED -> [alice, alice]
|
||||
Map<String, Account> ALLOWED -> {acct-1=Account[1,alice,100], acct-3=Account[3,alice,300]}
|
||||
Optional<Account> (alice's) DENIED -> IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was Optional[Account[1,alice,100]]
|
||||
Optional<Account> (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<Account> (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.
|
||||
58
method-security/docs/output/demo6.txt
Normal file
58
method-security/docs/output/demo6.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
==============================================================================
|
||||
Demo 6 -- interceptor order, and @PostAuthorize vs @Transactional
|
||||
==============================================================================
|
||||
|
||||
AuthorizationInterceptorsOrder, read from the enum itself
|
||||
---------------------------------------------------------
|
||||
CONSTANT getOrder()
|
||||
FIRST -2147483648
|
||||
PRE_FILTER 100
|
||||
PRE_AUTHORIZE 200
|
||||
SECURED 300
|
||||
JSR250 400
|
||||
SECURE_RESULT 450
|
||||
POST_AUTHORIZE 500
|
||||
POST_FILTER 600
|
||||
LAST 2147483647
|
||||
|
||||
Lower order = higher precedence = further OUT in the chain. Spring's own
|
||||
@Transactional advisor defaults to Ordered.LOWEST_PRECEDENCE (2147483647),
|
||||
which is larger than every number above -- so security wraps transactions,
|
||||
not the other way round.
|
||||
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.
|
||||
|
||||
The advisor chain on a bean carrying all four annotations
|
||||
---------------------------------------------------------
|
||||
advisors applied to the proxy: 4
|
||||
ORDER ADVISOR BEAN (as registered by @EnableMethodSecurity)
|
||||
100 preFilterAuthorizationMethodInterceptor
|
||||
200 preAuthorizeAuthorizationMethodInterceptor
|
||||
450 authorizeReturnObjectMethodInterceptor
|
||||
500 postAuthorizeAuthorizationMethodInterceptor
|
||||
600 postFilterAuthorizationMethodInterceptor
|
||||
(authorizeReturnObject sits at SECURE_RESULT = 450 and is registered
|
||||
whether or not anything in the app uses @AuthorizeReturnObject.)
|
||||
|
||||
For BEFORE advice, a lower order runs earlier: @PreFilter (100) really
|
||||
does run before @PreAuthorize (200). For AFTER advice the same numbers
|
||||
mean the opposite. @PostAuthorize (500) sits FURTHER OUT than
|
||||
@PostFilter (600), so on the way back out @PostFilter finishes first
|
||||
and @PostAuthorize evaluates returnObject on the ALREADY-FILTERED list.
|
||||
|
||||
Both methods below return the same 3 elements and filter one away:
|
||||
@PostAuthorize returnObject.size() == 3 DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@PostAuthorize returnObject.size() == 2 ALLOWED -> [a, b]
|
||||
|
||||
Default order: @PostAuthorize denies AFTER the transaction commits
|
||||
------------------------------------------------------------------
|
||||
rows before : 0
|
||||
recordAndReturn("bob") @PostAuthorize DENIED -> AuthorizationDeniedException: Access Denied
|
||||
rows after the denial : 1
|
||||
|
||||
@EnableTransactionManagement(order = FIRST): the write rolls back
|
||||
-----------------------------------------------------------------
|
||||
rows before : 0
|
||||
recordAndReturn("bob") @PostAuthorize DENIED -> AuthorizationDeniedException: Access Denied
|
||||
rows after the denial : 0
|
||||
34
method-security/docs/output/demo7.txt
Normal file
34
method-security/docs/output/demo7.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
==============================================================================
|
||||
Demo 7 -- @HandleAuthorizationDenied and @AuthorizeReturnObject
|
||||
==============================================================================
|
||||
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.
|
||||
|
||||
The exception type you actually catch
|
||||
-------------------------------------
|
||||
thrown -> org.springframework.security.authorization.AuthorizationDeniedException
|
||||
is AccessDeniedException -> true
|
||||
is AuthorizationDeniedException -> true
|
||||
carries an AuthorizationResult -> ExpressionAuthorizationDecision granted=false
|
||||
|
||||
Handlers written against AccessDeniedException still work -- but the
|
||||
concrete type carries the AuthorizationResult that explains the denial.
|
||||
|
||||
@HandleAuthorizationDenied: return something instead of throwing
|
||||
----------------------------------------------------------------
|
||||
maskedBalance() (alice, no ROLE_FINANCE) ALLOWED -> ***masked***
|
||||
maskedList() (alice, no ROLE_FINANCE) ALLOWED -> []
|
||||
maskedBalance() (cfo, has ROLE_FINANCE) ALLOWED -> 1,204,993.22
|
||||
|
||||
@AuthorizeReturnObject: the check moves onto the returned object
|
||||
----------------------------------------------------------------
|
||||
returned instance -> com.ankurm.methodsec.Demo7DeniedHandling$Customer$$SpringCGLIB$$0
|
||||
customer.getName() (no authority needed) ALLOWED -> alice
|
||||
customer.getEmail() (needs 'pii:read') DENIED -> AuthorizationDeniedException: Access Denied
|
||||
customer.getEmail() (has 'pii:read') ALLOWED -> alice@example.com
|
||||
|
||||
Same thing without the annotation, via AuthorizationProxyFactory
|
||||
----------------------------------------------------------------
|
||||
raw.getEmail() (unproxied object) ALLOWED -> alice@example.com
|
||||
wrapped.getEmail() (proxied object) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
47
method-security/docs/output/demo8.txt
Normal file
47
method-security/docs/output/demo8.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
==============================================================================
|
||||
Demo 8 -- meta-annotations, templates, class-level rules, ambiguity
|
||||
==============================================================================
|
||||
|
||||
A plain meta-annotation needs no extra configuration
|
||||
----------------------------------------------------
|
||||
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.
|
||||
@IsAdmin (alice, ROLE_USER) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@IsAdmin (root, ROLE_ADMIN) ALLOWED -> ok
|
||||
|
||||
A TEMPLATED meta-annotation, with NO AnnotationTemplateExpressionDefaults bean
|
||||
------------------------------------------------------------------------------
|
||||
@HasRole("ADMIN") as root (ROLE_ADMIN) ALLOWED -> ok
|
||||
@HasRole("ADMIN") as alice (ROLE_USER) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
'{value}' was substituted anyway. The reference documentation says you
|
||||
must publish an AnnotationTemplateExpressionDefaults bean for templated
|
||||
meta-annotations to work; in 7.1.1 you do not.
|
||||
PreAuthorizeExpressionAttributeRegistry initialises its scanner with
|
||||
SecurityAnnotationScanners.requireUnique(PreAuthorize.class), and that
|
||||
overload constructs a default AnnotationTemplateExpressionDefaults for
|
||||
you. Publishing the bean only changes ignoreUnknown.
|
||||
|
||||
The same annotation WITH the AnnotationTemplateExpressionDefaults bean
|
||||
----------------------------------------------------------------------
|
||||
@HasRole("ADMIN") as root (ROLE_ADMIN) ALLOWED -> ok
|
||||
@HasRole("ADMIN") as alice (ROLE_USER) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
@HasRole("USER") as alice (ROLE_USER) ALLOWED -> ok
|
||||
|
||||
Class-level rules, and what a method-level one does to them
|
||||
-----------------------------------------------------------
|
||||
inherited from the class (needs ADMIN) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
method-level overrides it (needs USER) ALLOWED -> ok
|
||||
class @PreAuthorize AND method @PostAuthorize DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
Two interfaces, two different @PreAuthorize on the same method
|
||||
--------------------------------------------------------------
|
||||
context started fine.
|
||||
bean type -> jdk.proxy2.$Proxy21
|
||||
read() -- inherits two conflicting rules DENIED -> AnnotationConfigurationException: Please ensure there is one unique annotation of type [interface org.springframework.security.access.prepost.PreAuthorize] attributed to public abstract java.lang.String com.ankurm.methodsec.Demo8MetaAnnotations$ReadsAsUser.read(). Found 2 competing annotations: [@org.springframework.security.access.prepost.PreAuthorize("hasRole('USER')"), @org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')")]
|
||||
|
||||
It is not a startup failure: the context refreshes, the bean is
|
||||
proxied, and the conflict only surfaces when the method is called.
|
||||
The fix is to put @PreAuthorize on the implementation method, which
|
||||
is the nearest declaration and therefore wins outright.
|
||||
24
method-security/docs/output/demo9-with-parameters.txt
Normal file
24
method-security/docs/output/demo9-with-parameters.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
==============================================================================
|
||||
Demo 9 -- #parameterName and the -parameters compiler flag
|
||||
==============================================================================
|
||||
compiled with -parameters : true
|
||||
byParameterName param[0] : owner
|
||||
byParameterAlias param[0] : owner (annotated @P("o"))
|
||||
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.
|
||||
|
||||
alice calling with her own name
|
||||
-------------------------------
|
||||
#owner == authentication.name ALLOWED -> ok
|
||||
#o == authentication.name (@P("o")) ALLOWED -> ok
|
||||
|
||||
alice calling with somebody else's name
|
||||
---------------------------------------
|
||||
#owner == authentication.name DENIED -> AuthorizationDeniedException: Access Denied
|
||||
#o == authentication.name (@P("o")) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
Without -parameters the first expression denies BOTH calls -- it fails
|
||||
closed, which is the good direction, but it fails silently in the sense
|
||||
that nothing tells you the rule is not the rule you wrote. @P("o") does
|
||||
not depend on the flag, because the name is in the class file either way.
|
||||
24
method-security/docs/output/demo9-without-parameters.txt
Normal file
24
method-security/docs/output/demo9-without-parameters.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
==============================================================================
|
||||
Demo 9 -- #parameterName and the -parameters compiler flag
|
||||
==============================================================================
|
||||
compiled with -parameters : false
|
||||
byParameterName param[0] : arg0
|
||||
byParameterAlias param[0] : arg0 (annotated @P("o"))
|
||||
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.
|
||||
|
||||
alice calling with her own name
|
||||
-------------------------------
|
||||
#owner == authentication.name DENIED -> AuthorizationDeniedException: Access Denied
|
||||
#o == authentication.name (@P("o")) ALLOWED -> ok
|
||||
|
||||
alice calling with somebody else's name
|
||||
---------------------------------------
|
||||
#owner == authentication.name DENIED -> AuthorizationDeniedException: Access Denied
|
||||
#o == authentication.name (@P("o")) DENIED -> AuthorizationDeniedException: Access Denied
|
||||
|
||||
Without -parameters the first expression denies BOTH calls -- it fails
|
||||
closed, which is the good direction, but it fails silently in the sense
|
||||
that nothing tells you the rule is not the rule you wrote. @P("o") does
|
||||
not depend on the flag, because the name is in the class file either way.
|
||||
6
method-security/docs/output/tests.txt
Normal file
6
method-security/docs/output/tests.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
mvn test -- MethodSecurityTrapsTest (14 tests pinning every claim the demos print)
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.ankurm.methodsec.MethodSecurityTrapsTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.076 s -- in com.ankurm.methodsec.MethodSecurityTrapsTest
|
||||
Reference in New Issue
Block a user