acs-i3x: clear the ground before adding access control - #710
acs-i3x: clear the ground before adding access control#710AlexGodbehere wants to merge 4 commits into
Conversation
The OAuth client guide cited acs-i3x as an example of calling back into the F+ auth service to authorize a user per object. It does not: no ACL check exists anywhere in the service. An integrator reading that would reasonably conclude a shared acs-i3x credential is scoped by F+ ACLs, when in fact it grants read access to the whole object tree. Describe what the service actually does - authenticates via Kerberos, opaque bearer or Keycloak JWT, and authorizes nothing - and drop the reference to an OAuth2 token-exchange flow the auth service does not implement.
Every other ACS service that performs ACL checks gets ROOT_PRINCIPAL; the service-client root bypass in fetch_acl is gated on it. i3x was missing it, so the first ACL check added to the service would lock the site administrator out. Add it ahead of enforcement landing, using the same admin@<realm> form as data-access.
The value endpoints printed lookup traces with raw console.log, and the
messages included the metric values themselves. That puts real plant
data into cluster logs, and console.log bypasses the buffered
per-request logger WebAPI installs on req.log - which is where the
authenticated principal for the request is recorded - so the values
landed with no identity attached.
Route these through req.log where present, falling back to a
fplus.debug.bound("api-v1") module logger, and drop the value payloads.
The element id, cache hit/miss and source timestamp are kept, which is
what the messages were actually useful for.
Every other non-bulk endpoint returns { success, result }; /info
returned the bare ServerInfo object because the envelope middleware was
attached to the main router only and the info route is mounted
separately so it can bypass auth and the readiness gate.
The i3X OpenAPI spec types the 200 response of GET /info as
SuccessResponse_ServerInfo_, so the envelope is correct and the service
was wrong; acs-admin's useI3xClient, which unwraps body.result and was
getting undefined for this route, needs no change.
The middleware is attached to the route rather than the router: both
routers are mounted on /v1, so a router-level use() would double-wrap
every other endpoint. Compliance test now includes /v1/info in the
"every success response is enveloped" sweep.
Review: four preparatory fixes before access control lands in acs-i3xBranch: What changed (functional level)
The published integration guide claimed the opposite of the truth, saying acs-i3x asks the auth service "what can principal
Value lookups no longer print process values to stdout, and now log through the per-request buffer where the caller's identity is recorded.
WalkthroughDocs: what acs-i3x actually does
Deployment: ROOT_PRINCIPAL
Logging: no more plant data on stdout
Envelope on /v1/info
How to test
Decisions / open questions
Files of noteDocs:
Deployment:
Service:
Tests:
|
Four preparatory fixes ahead of adding access control to
acs-i3x. No enforcement is added here - the service still authorizes nothing. This clears the ground so the enforcement PR is a single, reviewable change.1. The OAuth guide described authorization that does not exist
docs/auth/oauth-clients.mdcitedacs-i3xas an example of "Option B - call back into Factory+ with the user's identity", specifically minting a service-to-service token and asking "what can principal<uuid>do?".That is false.
grep -rn "check_acl\|fetch_acl\|req.auth" acs-i3x/lib acs-i3x/binreturns nothing. This is the harmful direction to be wrong in: it would lead an integrator to assume a shared acs-i3x credential is scoped by Factory+ ACLs when it grants read access to the entire object tree.Replaced with a "What acs-i3x does today" subsection that states what the service actually does:
FplusHttpAuthmiddleware - Kerberos (Negotiate, orBasicwith UPN + password), an opaque bearer fromPOST /token, or a Keycloak JWT carryingfp_principal_uuid;GET /v1/infois public, everything else requires auth;No date promised, and no overclaiming the other way - the service does authenticate.
Other claim fixed in the same file: the guide offered OAuth2 token exchange as an alternative under Option B, "once the i3x shim work lands". The F+ auth service does not implement token exchange today, so that has been replaced with a plain statement that a service-to-service call is the only option.
2.
ROOT_PRINCIPALon the i3x deploymentdeploy/templates/i3x/i3x.yamlwas the only ACL-relevant service without it. Addedadmin@{{ .Values.identity.realm }}, copyingdeploy/templates/data-access/data-access.yamlexactly.The point worth not losing: the client-side root bypass in
lib/js-service-client/lib/service/auth.js(fetch_acl, ~line 141) is Kerberos-UPN-only:FplusHttpAuth.auth_jwtsetsreq.authto thefp_principal_uuidclaim, which decodes as auuid-type principal, notkerberos. So a Keycloak-authenticated administrator can never hit the root bypass, no matter whatROOT_PRINCIPALis set to. Once enforcement lands, the admin either authenticates with Kerberos to get root, or needs an explicit ACL grant against their principal UUID. This should be decided deliberately in the enforcement PR rather than discovered in the field.3. Metric values in stdout at info level
acs-i3x/lib/api-v1.tsprinted value-lookup traces with rawconsole.log, including the metric values themselves - real plant data landing in cluster logs.Two fixes:
WebAPIinstalls a buffered per-request logger onreq.log(lib/js-service-api/lib/webapi.js~54-67), and that buffer is whereFplusHttpAuthrecords the authenticated principal.console.logbypasses it entirely, so the messages were unattributable. A smalllog_reqhelper prefersreq.logand falls back to a house-patternopts.fplus.debug.bound("api-v1")module logger, which is what the unit tests hit (they mount the routers on a bare Express app with noreq.log).debugis plumbedbin/api.ts->routes()->APIv1asfplus.debug, matching howObjectTreealready takes its logger.4.
GET /v1/infoenvelope - reasoningConclusion: the spec requires the envelope. The bug was in acs-i3x, not acs-admin.
acs-admin/src/composables/useI3xClient.jsunwrapsbody.resultfor every call includinggetInfo(), and was gettingundefined. Two candidate fixes; I checked rather than assumed.The evidence:
acs-i3x/docs/design.mdsays "Express middleware wraps all responses in the i3X envelope" andpitch.mditem 10 says "All responses wrapped in{ success: true, result: ... }" - butpitch.mditem 2 shows theGET /infoexample as a bare object, which is presumably where the implementation took its cue.https://api.i3x.dev/v0/openapi.json). It types the 200 response ofGET /infoasSuccessResponse_ServerInfo_, i.e.{ success, result: ServerInfo }- the sameSuccessResponsewrapper used by/namespaces,/objecttypes,/objects/{id}/valueand the rest. The only genuinely un-enveloped responses in the spec arePUT /objects/{id}/historyandPOST /subscriptions/stream, both of which return{}.So
/infois enveloped like everything else. The separateinfoRouteexists only so/infocan bypass auth and the readiness gate, which is orthogonal to the response shape - the envelope should always have applied to it.One implementation detail worth a look during review: the middleware is attached to the route, not via
infoRoute.use(). Both routers are mounted on/v1, so a router-leveluse()oninfoRouteruns for every request destined for the main router too and double-wraps them. I hit exactly this - 91 test failures - before switching to the per-route form.No change to
acs-admin; its unwrapping was already correct and now gets a defined value.Test results
Full
acs-i3xsuite, after the change:Baseline on the unmodified tree was also 13/13 and 342/342, so nothing was already failing and nothing regressed.
npx tsc --noEmitis clean.Test changes:
test/api-v1.test.tsandtest/e2e.test.tsupdated for the new/infoshape, and/v1/infoadded to the "every success response has{ success: true, result }" compliance sweep ine2e.test.ts- it was previously excluded with a comment explaining that the info route returns raw data. That exclusion is now gone.acs-i3x/dist/is gitignored (root.gitignore,dist/) and not tracked, so no rebuild was needed.Helm
helm lintandhelm templateboth pass and the deployment rendersROOT_PRINCIPAL: admin@EXAMPLE.COM. Rendered from a scratch copy of the chart with thedependencies:block stripped, because the subcharts (traefik, grafana, influxdb2, cert-manager) are not vendored intodeploy/charts/. No command was run against a live cluster.Noticed, not fixed
acs-i3x/lib/subscriptions.tshas the same problem as item 3: seven rawconsole.logcalls, and the one at line 216 printsvalue=${JSON.stringify(vqt.value)}for every SSE frame - i.e. plant data on the hot path, at higher volume than the ones fixed here. Left alone because another agent is working in that file.acs-i3x/docs/design.md(~line 262) claims "ACL checks viaAuth.check_acl()per request" under Authentication. Same false claim as item 1, in the service's own design doc. Out of scope for this PR, which was scoped todocs/auth/oauth-clients.md, but it should be corrected by whoever lands enforcement - by which point it may become true.deploy/templates/i3x/i3x.yamlhardcodesVERBOSE: ALL, unlike siblings which template it from averbosityvalue. WithALLevery debug tag prints, so the newapi-v1logger will be verbose in production. Worth templating, but it is a values-schema change and did not belong in this PR.How to test
cd acs-i3x && npm install && npm test- expect 13 suites, 342 tests, all passing.npx tsc --noEmit- expect no output.curl -s http://i3x.<baseUrl>/v1/info | jq. Expect{ "success": true, "result": { "specVersion": ..., "serverName": ..., "capabilities": {...} } }, not a bare object.curl -s -u <user> http://i3x.<baseUrl>/v1/namespaces | jq- expect{ success: true, result: [...] }withresultan array, notresult.result.getInfo()now resolves to the server info object instead ofundefined.GET /v1/objects/<id>/value) and check the pod logs. Expect lines likevalue <elementId>: UNS cache hit, ts ..., age 1234ms, grouped with the>>> GET .../Auth succeeded for [...]lines for that request. Expect no metric values in the output.helm templatethe chart and confirm the i3x Deployment carriesROOT_PRINCIPAL: admin@<your realm>.docs/auth/oauth-clients.mdand check it matches your reading of the code.Release notes
acs-i3x:GET /v1/infonow returns the i3X success envelope. The endpoint previously returned the bare server-info object while every other endpoint returned{ success, result }. This is a breaking change for any client that readspecVersionand friends off the top level of the response; read them from.resultinstead. The ACS admin UI already expected the enveloped form and was receivingundefinedfrom this route.acs-i3x: metric values are no longer written to the service log. Value lookups were logged with the process value included. They now log the element id, cache hit/miss and source timestamp only, and go through the standard per-request logger so entries are attributable to the authenticated caller.acs-i3x:ROOT_PRINCIPALis now set on the deployment, matching every other ACL-checking ACS service. No behavioural change today, since acs-i3x performs no ACL checks; this prepares for access control being added.