Skip to content

fix(grafana): user-project mapping proxy fix for v1.5.7-arbisoft - #44

Merged
sobankhalid101 merged 7 commits into
release-1.5.7-arbisoftfrom
bug/release-1.5.7-mapping-fix
Sep 8, 2026
Merged

sobankhalid101 merged 7 commits into
release-1.5.7-arbisoftfrom
bug/release-1.5.7-mapping-fix

Conversation

@sobankhalid101

Copy link
Copy Markdown

Summary

Release PR for v1.5.7-arbisoft. Cherry-picks the Grafana user-project-mapping fix onto release-1.5.7-arbisoft, which is branched from the v1.5.6-arbisoft tag — not from main-arbisoft.

Prod runs v1.5.6-arbisoft. main-arbisoft is 103 commits / 152 files ahead of it, carrying the Claude-Code OTel connector, the native-OIDC Access Directory, and a large config-ui refactor — none of which is ready for production. Tagging main-arbisoft would ship all of it. This branch carries the fix and nothing else.

What it fixes

The Admin: User Project Access dashboard's Add/Remove buttons return 404 on prod: they post to a hostname-less /api/user-project-mappings path, which on devlake-grafana.arbisoft.com resolves to Grafana's own API where no such route exists. They now go through Grafana's datasource proxy, which forwards to DevLake over internal DNS with a scoped API key attached server-side, so the key never reaches the browser.

Already merged to main-arbisoft as #40, #41 and #42 and running on staging. The cluster-side config is live in prod via arbisoft/hks-argocd#931.

Commits (7)

Commit Why
fix(auth): authorize webhooks when auth is enabled (apache#9008) Prerequisite — see below
fix(api): strip /rest from RawPath so encoded path params reroute UseRawPath=true makes gin re-route on RawPath, which was left prefixed with /rest; every email-shaped login 404'd
feat(api): require a Grafana org Admin for user-project mapping changes The datasource proxy admits any signed-in Grafana user, Viewers included
fix(grafana): route user-project mapping through the datasource proxy The datasource + the two panels
fix(grafana): safe JSON variable interpolation … use limit query param From #41
fix(grafana): read mapping dashboard variables at runtime ${var:json} returns bare strings for scalars → SyntaxError
style(grafana): trim the mapping panel comments

⚠️ Why the apache#9008 backport is included

It is not one of the fix commits, and it is not optional. It introduces shared.SetRestAuthUser and its call site. Without it:

  • gin_utils.go references an undefined restAuthKey and the build fails; and
  • if that were patched around, GetRestAuthUser would always return false, RequireGrafanaAdmin would take its pass-through branch for every request, and the Admin gate would be a silent no-op — any Grafana Viewer could grant themselves any project. Exactly the hole this release exists to close.

Verified present on this branch: call site at middlewares.go:225, accessors at gin_utils.go:36,44, gate reading it at grafanarole/middleware.go:43.

Cherry-pick notes

Applied cleanly except one conflict in env.example, resolved by keeping only the GRAFANA_BASE_URL / GRAFANA_SERVICE_ACCOUNT_TOKEN block and dropping OIDC_ALLOW_EMAILS / OIDC_ALLOW_DOMAINS, whose Go code does not exist at this tag.

The merged api.go and router.go came out correct for 1.5.6: grafanarole.Init sits after auth.Init with no access.Init, the middleware chain keeps 1.5.6's unconditional OAuth2ProxyAuthentication, and there are no access-package imports.

Leakage check: no files under config-ui/, telemetry/, backend/plugins/, or the access directory. 12 files, +806/−29.

Why this PR exists at all

Partly to run the tests. The fork's test.yml triggers on PRs to main and release-* — upstream Apache's branch names — so PRs to main-arbisoft only ever ran lint, and the Go unit tests in this fix have never executed anywhere. Targeting a release-* base is what finally makes them run:

  • grafanarole: Admin passes; Viewer/Editor, missing X-Grafana-User, unset config and unreachable Grafana all 403; a substring near-miss must not inherit an admin row
  • RawPath: %40, %2F, %20 and a plain path all reach their handler with params correctly unescaped

Please confirm test is green before merging.

After merge

Tag v1.5.7-arbisoft on release-1.5.7-arbisoft. That tag push builds all three images and deploys to production unattended — there is no gate after it, and ECR immutability means the tag name cannot be reused.

release-1.5.7-arbisoft stays as the 1.5.x hotfix line. No back-merge needed; these commits are already on main-arbisoft.

🤖 Generated with Claude Code

https://claude.ai/code/session_014QrN5pWnSEk3RCAh9v9ADf

kamphaus and others added 7 commits September 8, 2026 14:31
The API server runs with UseRawPath=true, so gin re-routes HandleContext on
URL.RawPath whenever it is non-empty. CheckAuthorizationHeader stripped the
/rest prefix from URL.Path only, leaving RawPath prefixed, so the replay
matched no route and returned gin's 404.

net/url sets RawPath only when the caller's escaping differs from Go's own
encodePath output. That is the case for the reserved characters
encodeURIComponent escapes but encodePath does not, notably "@" (%40) in an
email login and "/" (%2F) inside a segment; a space (%20) escapes
identically and was never affected.

No existing /rest caller has hit this because none passes an encoded path
parameter. The Grafana datasource proxy route added later in this branch
does, on every click, since the mapping key is an email address.
The user-project-mapping routes are about to become reachable through
Grafana's datasource proxy. That endpoint is authorized on
datasources:query, not on org role, so any signed-in Grafana user including
a Viewer can call it and would otherwise be able to grant themselves any
project.

Requests carrying the REST API-key identity must now also present
X-Grafana-User, and that user must hold the Admin org role, checked live
against Grafana's /api/org/users. Everything else fails closed: a missing
header, absent configuration, or an unreachable Grafana all yield 403.
Callers authenticated any other way pass through untouched, so config-ui
behaviour is unchanged; gating the session path is tracked separately,
alongside the /access/* routes that already do it.

The lookup matches on login OR email because Grafana sends the login, which
equals the email for SSO accounts but not for the migrated local ones, and
it requires an exact hit before trusting the role because Grafana's ?query=
is a substring search that can return an unrelated admin row.

Needs GRAFANA_BASE_URL and GRAFANA_SERVICE_ACCOUNT_TOKEN. The token needs a
Grafana service account with the Admin basic role, since Grafana OSS cannot
scope /api/org/users below it; give it a dedicated, separately revocable
account.
The Add/Remove buttons posted to a hostname-less
/api/user-project-mappings path, so the request followed whichever host
happened to serve Grafana. On prod that is Grafana's own hostname, where
/api is Grafana's API and no such route exists: 404. On staging it did
reach DevLake, but has returned 403 "csrf token missing or invalid" since
the AUTH_ENABLED cutover, because the panel sends no X-CSRF-Token.

Both are the same defect and both are fixed by calling Grafana's own
datasource proxy instead. Grafana forwards to DevLake over internal DNS and
attaches a scoped API key server-side, so the key never reaches the browser
and the call stays on Grafana's own origin: no CORS, no SameSite, no new
hostname or route, and DevLake stays off the public edge.

It also fixes the CSRF failure, though not because of the Bearer key: there
is no Bearer exemption in CSRFProtect, and the /rest rewrite runs the whole
middleware chain twice. Grafana strips every cookie from the proxied
request, so no devlake_session arrives and the check never engages. That
makes jsonData.keepCookies actively dangerous here; the provisioning
comment says so.

The URL is built from appSubUrl at click time because Grafana's API root is
"" on prod and "/grafana" on staging, so one file is now correct in both.
The panels move to the plugin's code-only update mode, which also replaces
the "Check DevLake API URL" toast with the real status and body.
…erpolation

Add/Remove Mapping threw SyntaxError before issuing any request:

  const userLogin = asghar.ali@arbisoft.com;
  const project = Corey Systems;

Grafana's json formatter returns a bare string unchanged -- it only
stringifies arrays -- so `${userLogin:json}` emitted an unquoted token and
the panel failed to compile the code on every real value.

Switching to a quoting formatter would only narrow the window: :doublequote
and :singlequote escape their own quote character but not backslashes or
newlines, so a login or project name carrying either still breaks out of the
literal.

The interpolation is removed instead. replaceVariables() is applied to the
whole of this code before it is compiled, so any variable value placed in
the source is a potential syntax error; reading through templateService at
runtime hands back a plain string that never passes through the compiler.

The variable name is concatenated so the read itself does not match
Grafana's variable regex. Written as one literal it would be substituted in
place and put the raw value straight back into the source, which is the bug
this commit removes. There is a comment on the line saying so.

Verified by simulating replaceVariables over both panels and syntax-checking
the result: the source is unchanged by interpolation, and both compile for
values containing an apostrophe or a backslash, each of which fails before
this change.
The rationale for reading variables at runtime belongs in the commit and the
PR, not in a code field edited through a small editor box in the panel
options. Sixteen comment lines against fifteen of code made the actual
request hard to find.

Cut to seven: what appSubUrl is for, a pointer to the datasource file that
already carries the full proxy and CSRF explanation, and the one warning
that has to survive -- keep the variable name split, or the line becomes a
variable reference and puts the raw value back in the source.

Also adds blank lines between the three sections (URL, variables, request)
and moves the two long fetch calls onto their own lines.

No behaviour change. Re-checked that neither panel's source contains
anything matching Grafana's variable regex, including in the comments, and
that both still compile after simulated interpolation with values carrying
an apostrophe, a backslash or a newline.
@sobankhalid101
sobankhalid101 merged commit cf91a03 into release-1.5.7-arbisoft Sep 8, 2026
16 of 17 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.

3 participants