Repository navigation
WW-5844 fix(core)!: enforce HTTP method annotations across action hierarchies and plugin stacks - #2120
WW-5844 fix(core)!: enforce HTTP method annotations across action hierarchies and plugin stacks#2120g0w6y wants to merge 2 commits into
Conversation
…nd add httpMethod to the REST and Bean Validation default stacks HttpMethodInterceptor misses an @http* annotation in four distinct, independent ways: a method-level annotation on the implicit execute() method (gated by isMethodSpecified(), while the class-level check runs unconditionally), a class- or method-level annotation lost to a subclass that doesn't repeat it (plain Java annotation semantics, none of the Http* annotations is @inherited), and the same loss one level further out for an annotation declared on an interface reached only through an intermediate superclass. restDefaultStack and beanValidationDefaultStack are the only two bundled interceptor stacks anywhere in core or any plugin that carry params but omit httpMethod entirely. HttpMethodInterceptor now always resolves the invoked method, including the implicit execute() case, and walks the full class/superclass/interface hierarchy for the nearest annotation at both the method and class level, checking every level's own implemented interfaces. httpMethod added to both stacks. A new, generic AccStackInvariantsTest (core) walks every stack definition in struts-default.xml and asserts any stack carrying params also carries httpMethod. Tests: AccHttpMethodTest, AccStackInvariantsTest (new), plus RestStackHttpMethodEnforcementTest and BeanValidationStackHttpMethodEnforcementTest. Full suites: core 3749/0 failures (2 skipped), rest 245/0, bean-validation 25/0, convention 94/0, json 177/0.
lukaszlenart
left a comment
There was a problem hiding this comment.
Thanks for the patch and the thorough test set. I applied the whole diff locally: all new tests pass, the existing HttpMethodInterceptorTest (29) and the WW-5718 stack pins stay green, and both plugin stacks return 405 for a rejected GET. The core fix is right. What blocks the merge is below, with the details inline.
Blocking
- Nine new files have no ASF license header, which is why the RAT check fails and the build is red: the three
Acc*test classes,struts-acc-http.xml, the REST plugin'sPostOnlyAction,RestStackHttpMethodEnforcementTest, and bothww-httpmethod-stack.xmlfiles. The Bean Validation test classes have the header, so that is the form to copy. - Two test Javadocs reference files on your machine (
~/hunt/reports2/...). Please remove them; anything worth keeping from those notes belongs in the PR body. - The new
org.apache.struts2.accesspackage and theAcc*names do not fit the tree. Please move the tests next to the interceptor underorg.apache.struts2.interceptor.httpmethodand name them by what they check (for example a hierarchy test and a default-stacks test).
Scope and behaviour
- The hierarchy walk and the two stack additions change behaviour for existing applications beyond the implicit
execute()case in the title: an annotation on a base class or an overridden method now applies to subclasses that were served before, and applications onrest-defaultorstruts-bean-validationstart enforcing annotations they currently ignore. That needs afix(core)!:marker and a short migration note in the PR body, or those parts split into their own PR. The docs will also need updating, since the action-configuration page lists exactly the three core stacks as enforcing. - Adding
httpMethodtorestDefaultStackmeets the REST mapper's form tunnelling, see the inline comment onstruts-plugin.xml.
Smaller
- Comments: the six-line history block in
intercept()and the three Javadocs that are longer than the methods they describe should shrink to a line each; the commit message already carries the reasoning. - The plugin enforcement tests assert 405 for the core control but only "not invoked" for the plugin stack. 405 holds in both, so assert it symmetrically.
- Please shorten the PR title to the repo form, for example
WW-5844 fix(core): enforce method-level HTTP method annotations on the implicit execute(). It becomes the squash commit headline.
| if (invocation.getProxy().isMethodSpecified()) { | ||
| Method method = action.getClass().getMethod(invocation.getProxy().getMethod()); | ||
| if (AnnotationUtils.isAnnotatedBy(method, HTTP_METHOD_ANNOTATIONS)) { | ||
| // Resolve the method that is actually about to be invoked - including the implicit "execute" that |
There was a problem hiding this comment.
This block narrates the previous behaviour. One line at most, or nothing: the test and the commit message carry the history.
| } | ||
| try { | ||
| return actionClass.getMethod(methodName); | ||
| } catch (NoSuchMethodException e) { |
There was a problem hiding this comment.
Swallowing NoSuchMethodException changes what a typo in method= looks like: before, the interceptor threw and the configuration error surfaced on the first request; now a GET on a class-annotated action gets a 405 and a POST fails later inside the invocation with a less specific error. Either rethrow here, or state in the PR why falling through is wanted and add a test for it.
| if (found != null) { | ||
| return found; | ||
| } | ||
| for (Class<?> iface : type.getInterfaces()) { |
There was a problem hiding this comment.
getInterfaces() gives the interfaces this type declares, not their super-interfaces. An annotation on interface PostOnly { @HttpPost default String save() } reached through interface Audited extends PostOnly is not found. Either walk the transitive set (core has AnnotationUtils.addAllInterfaces / ClassUtils.getAllInterfaces) or drop the "full hierarchy" claim from the Javadoc and the PR body.
|
|
||
| private AnnotatedElement declaredOverrideAnnotatedIn(Class<?> type, Method method) { | ||
| try { | ||
| Method candidate = type.getDeclaredMethod(method.getName(), method.getParameterTypes()); |
There was a problem hiding this comment.
getDeclaredMethod also returns private, static and package-private methods, which are not overrides of the public action method. A base class with a private helper save() carrying an annotation would impose it on the subclass's public save(). Filter the candidate to public and non-static, which is what getMethod matched before.
| <interceptor-ref name="exception"/> | ||
| <interceptor-ref name="alias"/> | ||
| <interceptor-ref name="servletConfig"/> | ||
| <interceptor-ref name="httpMethod"/> |
There was a problem hiding this comment.
The REST mapper treats a POST carrying _method=put or _method=delete as PUT or DELETE and routes it to update/destroy (RestActionMapper.isPut/isDelete). This interceptor compares the raw request method, so once it runs in this stack, @HttpPut on update() or @HttpDelete on destroy() rejects every browser form that uses the plugin's own tunnelling with a 405. Today nothing breaks, because the annotations are ignored here, but the new enforcement is unusable with the documented form shape. Either honour the same override the mapper honours, or document that these annotations only work with real PUT/DELETE requests under rest-default.
|
|
||
| /** | ||
| * Guard test for the "bundled stack omits httpMethod" class (WW-5718 only resynced coep/coop/fetchMetadata; | ||
| * httpMethod and csp were left out of restDefaultStack/beanValidationDefaultStack - see ~/hunt/reports2/plugins2.md |
There was a problem hiding this comment.
References to ~/hunt/reports2/... are files on your machine; nobody else can follow them. Please remove them and keep the Javadoc to what the test asserts.
|
|
||
| /** Stacks that are deliberately bare (never bound to a package's default-interceptor-ref as its primary | ||
| * request stack; building blocks or dev-tool stacks only) are allowlisted here, by design, not by accident. */ | ||
| private static final Set<String> ALLOWED_BARE = Set.of(); |
There was a problem hiding this comment.
An empty allowlist plus a comment explaining it is machinery nothing uses. Drop it until a stack needs it.
| .withServletRequest(new MockHttpServletRequest(verb, "/" + action)) | ||
| .withServletResponse(res).getContextMap(); | ||
| AccHttpActions.RAN.set(0); | ||
| ActionProxy p = actionProxyFactory.createActionProxy("", action, null, ctx, false, true); |
There was a problem hiding this comment.
executeResult=false means the seven httpheader result mappings and the result type in struts-acc-http.xml are never used; please remove them. And assert the rejection itself, response.getStatus() == 405 plus the Allow header as HttpMethodInterceptorTest does, rather than only that the counter stayed at zero.
| } | ||
|
|
||
| public void testRestDefaultStackRejectsGetOnPostOnlyAction() throws Exception { | ||
| getAndReportStatus("/rest"); |
There was a problem hiding this comment.
The core control above asserts 405; this one only checks the action did not run. 405 holds here too, so assertEquals(405, getAndReportStatus("/rest")). Same for the Bean Validation copy.
Resolve parent interfaces transitively, ignore unrelated non-public and static method declarations, and preserve annotations on the resolved instance method. Keep explicit missing-method errors while allowing normal implicit invocation fallback after class and XML method restrictions. Move and license the test fixtures, assert 405/Allow and accepted controls, and cover actual REST mapper routing with native and tunneled requests. Document that REST restrictions check the transport method. BREAKING CHANGE: inherited HTTP method annotations and implicit execute() annotations are now enforced. REST and Bean Validation default stacks now enforce HTTP method restrictions. POST-based REST forms must allow POST in handler annotations and any XML http-method restriction.
Fixes WW-5844
Checks method annotations on implicit
execute()and follows annotated class, superclass and interface declarations, including parent interfaces. An annotation on the resolved public instance method takes precedence. Unrelated private, package-private and static declarations do not impose restrictions.Adds
httpMethodtorestDefaultStackandbeanValidationDefaultStack. Rejection tests check 405,Allowand that the action did not run, with accepted-request controls. Missing explicitly configured methods still throwNoSuchMethodException; an absent implicitexecute()continues through normal invocation after class/XML restrictions are checked.Compatibility: inherited annotations and annotations on implicit
execute()now apply where they were previously ignored. The two plugin stacks also begin enforcing annotations and XMLhttp-methodrestrictions. Review base-class and interface annotations and declare the intended methods explicitly on overrides where needed.REST restrictions use the actual HTTP request method.
_method=put/deletechanges routing only:@HttpPut/@HttpDeletealone require real PUT/DELETE requests. For POST forms, add@HttpPostonupdate()/destroy()(alongside the native annotation if both transports are supported), and include POST in any XMLhttp-methodrestriction. Mapper-integrated tests cover native requests, rejected POST tunnelling and the POST-enabled controls.Validation: fresh reactor run passed: core 3761 tests (2 skipped), REST 254, Bean Validation 26, Convention 94 and JSON 177, with no failures or errors. RAT passed for the affected modules and their parent modules. The 49 focused core tests produce 9 assertion failures against the original PR interceptor and all pass with this revision.