Repository navigation
[#1075] Serve /xacml through CHF and delete its Restlet code - #1214
maximthomas merged 13 commits into
Conversation
…thod and send Allow on every 405
…F annotated service
…nses and pin the delegation action
…ete its Restlet wiring
…ers and add the upgrade note
…n the upgrade note
…he export 500 body in unit tests
…ured answers An invalid Accept-API-Version value answers 400 on CHF, where Restlet answered 500.
…nd pin the permission failure
The export wrote the realm-derived filename of Content-Disposition raw. Restlet's
Disposition quoted any value that is not an RFC 7230 token, and realm names created
through ssoadm or the legacy console may hold a space, ';' or '"' (REST allows
'[', ']', '{', '}'). Such a name is now sent as a quoted-string again.
New unit tests drive the SSOException and DelegationException arm of
checkPermission(Context, String), which answers 500 XML.
… parsing and complete the /xacml note The console form content type test now posts a document holding a '%' that two hex digits do not follow, so it fails if CHF ever parses that body as a form; the same body sent with the bare form content type answers 500. The spec header no longer describes the move off Restlet as pending. The upgrade note lists the changed <message> texts of the XML error documents and the removed @nAmed("XacmlRouter") Guice binding.
vharseko
left a comment
There was a problem hiding this comment.
Review of PR #1214 at head a639fdb.
[P2] Decode Content-Encoding before passing the import body to the XML parser
Location: openam-entitlements/src/main/java/org/forgerock/openam/xacml/v3/rest/XacmlService.java:153–154.
A POST to /xacml/policies with Content-Type: application/xml and Content-Encoding: gzip now passes compressed bytes directly to the XACML parser. getRawContentInputStream() deliberately performs no content decoding. The previous Restlet Application enabled DecoderService by default, so the Representation supplied to importXACML had already been decompressed. Clients that compress policy imports therefore lose working functionality after this migration: the new handler returns 400 with “XML cannot be parsed.” for otherwise valid XML.
I reproduced the transport/parser difference in an isolated Java harness using Restlet 2.4.4, this PR’s XacmlService, and the real XACMLReaderWriter. Restlet’s default application chain decoded gzip back to the original XML and the parser accepted it. With authorization and policy persistence stubbed, the new handler accepted the plain XML (200) but returned 400 for its gzip-compressed equivalent. This was not a full-server HTTP test.
Please read the body through Entity.newDecodedContentInputStream(), close that stream appropriately, and handle decoding/I/O failures as client errors. Add a regression case that posts a gzip-compressed valid policy document and checks the same import result as the uncompressed document. This regression is separate from the bare form-content-type issue already documented in the PR.
Validation:
- Read the PR description and complete discussion, plus #1075 and the related migration/characterization PRs #1201, #1202 and #1211. No published discussion on #1214 addresses compressed imports.
- 39 selected unit tests passed: EndpointsTest (8), RestEndpointServletTest (2 enabled tests), XacmlServiceTest (21), XacmlHttpRouteProviderTest (8).
- node --check e2e/xacml/xacml-test.spec.mjs and git diff a13a83c a639fdb --check passed.
- Docker Desktop was started successfully, but no configured OpenAM container was available; I did not run the full e2e suite or build a server WAR.
No other confirmed findings. The documented divergences and the explicitly deferred form-body issue are not being raised again. I recommend changes for the compressed-import regression.
…mport body The Restlet application decoded a compressed request entity through its default DecoderService; the CHF handler passed the raw bytes to the XACML parser, so a gzip-compressed import answered 400 "XML cannot be parsed." XacmlService now reads Entity.newDecodedContentInputStream() and closes it once the import returns. An unsupported coding answers 400 "Unsupported Content-Encoding: <coding>"; a body that is not in the declared format, for example "Not in GZIP format", answers 400 with that message, as Restlet's IOException branch did. CHF's DeflateDecoder reads "deflate" as raw DEFLATE, not the zlib format that Restlet read, and CHF has no decoder for Restlet's deflate-no-wrap, zip and identity; the upgrade note lists these. Fixing DeflateDecoder belongs in commons.
|
Confirmed and fixed in bbdce70. Restlet 2.4.4's
Not at parity, and now listed in the PR body and the upgrade note: CHF 3.2.2 decodes only |
vharseko
left a comment
There was a problem hiding this comment.
Re-review of PR #1214 at head bbdce70.
The previous P2 finding is addressed. XacmlService now reads the import body through Entity.newDecodedContentInputStream() after checking MODIFY permission and closes the decoded stream in a finally block. Unsupported content encodings and I/O failures while opening the stream produce HTTP 400 XML responses. Failures during XML parsing still follow the existing EntitlementException path.
The four new unit tests cover gzip decoding, stream closure, an unsupported encoding, and a body that is not in its declared gzip format. The new e2e case compresses a real export, imports it with dryrun=true into the test sub-realm, checks the expected import step, and verifies that the policy was not persisted.
I reviewed the full PR discussion, the migration constraints in #1075, the related characterization work, and OpenIdentityPlatform/commons#385. The documented deflate/other-encoding differences and the previously deferred bare form-content-type issue are not being raised again.
Validation on this head:
- 43 selected unit tests passed on JDK 21: EndpointsTest (8), RestEndpointServletTest (2), XacmlServiceTest (25), and XacmlHttpRouteProviderTest (8), with no failures, errors, or skips. The Maven reactor build completed successfully.
- node --check e2e/xacml/xacml-test.spec.mjs and git diff a13a83c bbdce70 --check passed.
- I did not run the full-server e2e suite or repeat the author's mutation runs. CI was still pending when checked.
No further confirmed findings. I recommend approval of the code change; merge should wait for CI to pass.
Summary
This is slice S4 of the sequential Restlet to CHF migration tracked in #1075. The
/xacmlpolicy import/export endpoint moves from theForgeRockRestservlet (Restlet) to theOpenAMservlet (CHF), next to/json, and its Restlet wiring is deleted. The wire format is kept except for the divergences listed below.Changes
Endpointsserves aHEADrequest with theGETmethod and puts anAllowheader on every 405 response (AnnotatedMethod.isSupported()added for this).XacmlServiceis a plain annotated CHF handler (@Post,@Get) instead of a Restlet resource.XacmlHttpRouteProviderregisters/xacmlwith CAF authentication,HttpContextFilter, the resource API version filter and a protocol 1.0 enforcement filter, and routes realms the way/jsondoes (/xacml/policies,/xacml/<realm>/policies,/xacml/realms/root[/realms/<sub>]/policies)./xacml/*to theOpenAMservlet;RestEndpointServletkeeps only/oauth2and/uma(itsRestletAuthnHttpApplication,RestletHandlerandHttpServletWrapperinner classes go) and answers 404 to any other servlet path, so a customizedweb.xmlthat still maps/xacml/*toForgeRockRestgets 404, not an empty 200; six classes are deleted:XacmlRouterProvider,XACMLServiceEndpointApplication,XMLRestStatusService,ResourceApiVersionRestlet,ResourceApiVersionRoutingFilter,RestletRouter(plusXMLRestStatusServiceTest).e2e/xacml/xacml-test.spec.mjsmoves to CHF's answers and gains three tests; the upgrade guide gets an[IMPORTANT]note.Divergences produced by the CHF code
GET /xacml/realms/root/policiesGET /xacml/realms/root/realms/<sub>/policiesContent-API-Versionvalue[Content-API-Version: protocol=1.0,resource=1.0]protocol=1.0,resource=1.0HEAD /xacml/policiesHEAD cause on master (verified on a running master container): Restlet answers HEAD with 200 and its headers but no body, so the servlet response stays uncommitted.
RestletHandlerthen returns null, CAFAuthenticationFramework.grantAccessthrows an NPE, andHttpFrameworkServletlogs "RuntimeException caught" and writesResponses.newInternalServerError()(setStatus(500)) over the 200. For a GET the body has already committed the response, so the late 500 is ignored.Unpinned changes
Measured on a master and a branch container with an admin token.
?realm=: 404application/xmlerror -> 400application/json(Invalid realm, /nosuchrealm)./xacml/nosuchrealm/policies): 404 XML -> 404 with an empty body./xacml/realms/root/realms/nosuchrealm/policies): 404 XML (naming the segment/realms) -> 404 JSON (Realm "/nosuchrealm" not found).Accept-API-Version: resource=x: 500 XML -> 400 plain text (Version string is in an invalid format: resource=x).PUT /xacml/policies: 405 withAllow: GET, POSTon both; the body changes from XML to JSON andContent-API-Versionloses the brackets.Endpoints:HEAD /json/apiwas 405 and is now answered likeGET;PUT /json/api405 now carriesAllow: GET;GETandPUT /json/authenticate405 now carryAllow: POST. In the test standGET /json/apiitself answers 500 with an empty body on both builds, soHEADshows 500 on the branch; that is existingApiServicebehaviour, not part of this PR.Server: Restlet-Framework/2.4.4header on/xacmlresponses.<message>of the XML error documents thatXacmlServicewrites: master's RestletResourceExceptioncarriedStatus.toString(), so an import without policies answeredNo policies found in XACML document (400); the branch writesNo policies found in XACML document. The text of a failed import follows the request'sAccept-Languageinstead of the server locale. Traced through the Restlet 2.4.4 bytecode (StatusService.getStatus,ResourceException(Status),RestStatusService), not measured; listed in the upgrade note.Content-Encodingof an import body:gzipis decoded on both (e2e test below). Restlet'sDecoderServicealso accepteddeflatein the zlib format,deflate-no-wrap,zipandidentity(passed through); CHF 3.2.2 has decoders forgzipanddeflateonly, and itsDeflateDecoderreads raw DEFLATE (new Inflater(true)). So a zlibdeflatebody answers 400XML cannot be parsed.instead of being imported, a raw DEFLATE body sent asdeflate(400 on master) is imported, anddeflate-no-wrap,zipandidentityanswer 400Unsupported Content-Encoding: <coding>. A coding neither side knows, such asbr, answers 400 on both,XML cannot be parsed.on master andUnsupported Content-Encoding: bron the branch. Traced through the Restlet 2.4.4 and CHF 3.2.2 bytecode, with the zlib case reproduced in jshell (ZipException: invalid stored block lengths); not measured on a container. Listed in the upgrade note.Known edges, not fixed here
CHF's global
ApiDescriptorFiltercallsrequest.getForm()on every request. An import posted with a bareContent-Type: application/x-www-form-urlencoded(for examplecurl --data @file.xmlwithout-H) whose XML contains a%not followed by two hex digits answers 500 without a body. The console sends; charset=UTF-8, which CHF does not parse as a form, so it is not affected (pinned by an e2e test whose document holds such a%; on the branch container the same body answers 500 with the bare content type and 200 with; charset=UTF-8). Proposed follow-up: read_apiwithnew Form().fromRequestQuery(request).CHF's
DeflateDecoderreadsContent-Encoding: deflateas raw DEFLATE, where RFC 9110 section 8.4.1.2 defines the zlib format (see theContent-Encodingbullet above). Under the #1075 rule that CHF defects are fixed in commons, that fix is tracked in OpenIdentityPlatform/commons#385;XacmlServicealready decodes through CHF and picks it up.Not changed (decision)
A request without a session still answers 500 XML. The 401 target is tracked separately.
Tests
Unit tests, all with 0 failures:
EndpointsTest8 (module total 17)XacmlServiceTest25,XacmlHttpRouteProviderTest8 (module total 616)Four new e2e tests (23 in the spec):
answers HEAD like GET without a body(a divergence: master 500)reads a document posted with the console's form content type(parity: passes on master; its document holds a%that two hex digits do not follow, so it fails if CHF ever parses the body as a form)resolves a sub realm under the realms/root path spelling(nested realms)reads a gzip-compressed document(parity: passes on master, where Restlet'sDecoderServicedecoded the body)Runtime evidence
Docker stand:
openidentityplatform/openamimage with the WAR replaced, embedded config store,xacml/xacml-testrun twice on one container.RuntimeException caughtindebug/org.forgerock.http.servlet.HttpFrameworkServlet: master 68, branch 0 (the file is absent)EnforcedProtocolVersionFilterremoved from the chain): 20 passed / 2 failed in both runs; the twoContent-API-Versiontests fail with Received"resource=1.0"XacmlServiceclass patched in, master WAR, same spec): branch 22 passed / 0 failed and master 16 passed / 6 failed in both runs, the same six tests as above. On the branch container a realm namedxq[1]exports withContent-Disposition: attachment; filename="xq[1]-realm-policies.xml", quoted as master's RestletDispositiondid; the root realm keepsattachment; filename=realm-policies.xml.XacmlServiceclass patched in 23 passed / 0 failed in both runs; the branch WAR before the fix 22 passed / 1 failed in both runs, onlyreads a gzip-compressed document(Expected 200, Received 400); master WAR 17 passed / 6 failed in both runs, the same six tests as above, so the gzip test passes on master.HttpRouterProviderorXacmlHttpRouteProviderincatalina.out;/json/authenticateissued the admin token. TheEntitlementdebug log holds only policy-evaluation errors (a combining algorithm URN looked up as a class name, ClassNotFoundException) and dropped PolicySet notifications, as on master; none come from routing.Upgrade note
New
[IMPORTANT]block inopenam-documentation/openam-doc-source/src/main/asciidoc/upgrade-guide/chap-upgrade-servers.adoc: a customizedweb.xmlmust move the/xacml/*mapping to theOpenAMservlet, the behaviour changes above, the removed classes and the removed@Named("XacmlRouter")Guice binding of the RestletRouter.