8000 fix(ess): authenticate and durably record ChartArtifact provisioning by AntonTyb · Pull Request #809 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

fix(ess): authenticate and durably record ChartArtifact provisioning - #809

Merged
AntonTyb merged 4 commits into
feat/chart-artifact-examplefrom
fix/ess-chart-provisioning-lifecycle
Sep 9, 2026
Merged

AntonTyb merged 4 commits into
feat/chart-artifact-examplefrom
fix/ess-chart-provisioning-lifecycle

Conversation

@AntonTyb
@AntonTyb AntonTyb commented Sep 7, 2026
Copy link
Copy Markdown
Collaborator

What

Stacked on #676 (base branch feat/chart-artifact-example). Slice 1 of 4 from the split of
#710, requested in review.

Two defects in dashboard_observer.go that leave a chart artifact permanently broken. Both
predate #676; they were invisible because nothing in the UI rendered the artifact's
error_message.

1. CreateChart was never authenticated

Every chart artifact came back FAILED with

rpc error: code = Unauthenticated desc = failed to authenticate user:
missing authentication token in metadata: rpc error: code = Unauthenticated desc = unauthenticated

The Dashboard Service connection is dialed insecure with no per-RPC credentials
(prod.go:initDashboardClient), and grpc-go does not carry incoming metadata onto outgoing
calls, so the observer's CreateChart arrived with no token at all and createchart.go
rejected it.

Giving the Experiment State Server its own identity would not have fixed it: CreateChart
calls authorizeExperimentWrite(ctx, user, xid), authorizing the user's WRITER role on
the experiment, so the user's own token is the one that has to make the trip.
withForwardedAuth copies x-goog-iap-jwt-assertion / authorization off the incoming
context onto the outgoing one. When there is nothing to copy it logs a warning and calls
anyway — whether an anonymous call is acceptable is the Dashboard Service's decision, not the
observer's.

2. The status write reused the context that bounded CreateChart

If CreateChart spent its full two minutes, the UpdateArtifact that records the outcome
failed before reaching Spanner. FAILED never persisted and the artifact sat in
PROVISIONING for good — the one state where the operator most needs to know what happened is
the one state that could not be written down. The write now runs under
context.WithoutCancel with its own 15s deadline.

Testing

gofmt clean, golangci-lint exit 0.

Not run locally: Go tests. The import paths here are Bazel-mapped
(xmanager_cloud/dashboard_service/proto/api_go_proto and friends have no on-disk Go package)
and Bazel is not installed in this environment, so go build/go test fail on every import.
CI is their first real run.

New tests in dashboard_observer_test.go:

  • TestWithForwardedAuth — both header names, a non-credential header that must not be
    forwarded, no incoming metadata, and incoming metadata with nothing worth copying.
  • ..._ForwardsCallerCredentials — the credentials reach the client, captured off the
    outgoing context by the mock.
  • ..._UncredentialedCallerStillCalls — the call is issued rather than dropped.
  • ..._PersistsOutcomeAfterDeadline — checked to fail against the old code: with an
    already-cancelled governing context the status write still lands.

Notes for the reviewer

  • Credential forwarding is hand-rolled. No PerRPCCredentials or interceptor pattern
    exists anywhere in this repo to reuse, and only one outbound call needs it. If more
    server-to-server hops appear this belongs in a client interceptor instead.
  • Not UI-visible on its own. NewDashboardObserver is registered at prod.go:156, so this
    is a live production call path, not dead code, and the behaviour is observable through the
    artifact's stored status. But no UI surface draws a work-unit-parented chart until slice 3:
    dashboards_page.ts:130 is the only listCharts caller and it lists by dashboard parent.
  • Deterministic ChartId not taken. Raised on feat(ui): render ChartArtifact plots full-width on the work unit page #710. It does not stop an orphaned chart —
    there is no retry path here, so it would only make a future retry idempotent. Worth doing
    separately.

Stack

Branch Scope
feat/chart-artifact-example (#676) the example
1 fix/ess-chart-provisioning-lifecycle this PR
2 feat/ess-chart-artifact-plot-specs plot specs on the provisioned chart
3 feat/ui-chart-artifact-view full-width rendering, happy path
4 feat/ui-chart-artifact-view-states notices and error recovery

The four slices together reproduce #710 byte-for-byte
(git diff between the top of the stack and #710's head is empty).

TAG=agy
CONV=96ac94bd-c2b9-4753-b54f-b0b6b02d801f

The observer dialed the Dashboard Service with insecure credentials and no
per-RPC credentials, and grpc-go does not propagate incoming metadata onto
outgoing calls. CreateChart therefore arrived anonymous and came back
Unauthenticated, which the UI rendered in place of the chart.

withForwardedAuth copies the caller's IAP assertion or bearer token from the
incoming metadata onto the outgoing context. The server's own identity would
not do: CreateChart authorizes against the *user's* WRITER role on the
experiment, so the user's token is the one that has to make the trip.

An uncredentialed call still goes out -- whether to accept it is the Dashboard
Service's decision, not the observer's -- but it logs a warning first, which is
far easier to act on than the error it turns into.

TAG=agy
CONV=96ac94bd-c2b9-4753-b54f-b0b6b02d801f
The goroutine bounds itself to two minutes and then wrote the artifact's status
under that same context. When CreateChart is what spent the budget, the write is
refused before it reaches Spanner, so the FAILED status never lands: the
artifact sits in PROVISIONING for good, and the UI keeps reporting a chart that
is "still being provisioned" by a goroutine that exited long ago.

context.WithoutCancel detaches the write and gives it its own 15-second budget,
so the outcome is recorded whichever way the RPC went.

The test drives OnArtifactCreated with an already-cancelled context and asserts
the write context is still live and the status persisted is FAILED.

TAG=agy
CONV=96ac94bd-c2b9-4753-b54f-b0b6b02d801f
@gemini-code-assist gemini-code-assist Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces credential forwarding (such as authorization and IAP assertion headers) from incoming metadata to outgoing metadata when calling the Dashboard Service, and ensures that artifact status updates are persisted under a separate context timeout even if the main call times out. The review feedback highlights a potential issue where metadata.NewOutgoingContext completely overwrites any existing outgoing metadata (e.g., tracing headers), and suggests copying the existing outgoing metadata instead. Corresponding test updates are also recommended to verify that existing outgoing metadata is preserved.

…ng auth

withForwardedAuth built a fresh metadata.MD and handed it to
NewOutgoingContext, which replaces whatever the context was already carrying
rather than adding to it.

Nothing is lost today -- this is the only NewOutgoingContext in the tree and
the Dashboard connection is dialed without interceptors -- but a tracing or
request-ID interceptor added later would have its headers silently dropped on
this one call, which is a hard thing to go looking for.

Copy the existing outgoing metadata and set the credentials on the copy. The
"no credentials to forward" signal moves off len(outgoing), which is no longer
zero when the context arrives carrying something else, onto an explicit flag.

TAG=agy
CONV=96ac94bd-c2b9-4753-b54f-b0b6b02d801f
@AntonTyb
AntonTyb marked this pull request as ready for review September 8, 2026 08:43
@andrzejfudro andrzejfudro left a comment
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one question, one smaller request and two optional.

Review round 3 on #809.

resolveUserEmail in internal/auth prefers x-goog-user-email over the token's
own email claim, so a call that carried the header lost it on the way to
CreateChart and was authorized against a different principal than the rest of
the caller's requests. It is forwarded now, but on a separate list: it is an
identity hint the Dashboard Service only honors next to a valid token, so it
does not count as credentials and does not suppress the missing-credentials
warning on its own.

An empty header value is also treated as absent, matching how jwtFromMetadata
reads the same headers. Previously it was forwarded and reported as
credentials, which silenced the warning for a call that had none.

TAG=agy
CONV=96ac94bd-c2b9-4753-b54f-b0b6b02d801f
@AntonTyb
AntonTyb added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 5fc84b7 Sep 9, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0