PMM-14880 anonymous role#5170
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## PMM-14880-rta-pmm-demo #5170 +/- ##
==========================================================
- Coverage 47.79% 47.49% -0.30%
==========================================================
Files 410 411 +1
Lines 41974 42158 +184
==========================================================
- Hits 20062 20025 -37
- Misses 19935 20030 +95
- Partials 1977 2103 +126
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
We need profiling on this one. |
There was a problem hiding this comment.
Pull request overview
Enables “anonymous mode” users to receive an organization role (clamped to Viewer when Grafana is configured with a higher anonymous role), and updates PMM UI/back-end wiring to use new PMM-managed “current user” endpoints rather than calling Grafana’s user endpoints directly.
Changes:
- Added
/v1/users/currentand/v1/users/current/orgsHTTP endpoints in pmm-managed that proxy/normalize Grafana “current user” payloads and support anonymous fallback. - Extended Grafana client/auth logic to detect anonymous mode via
/api/frontend/settingsand clamp anonymous org role to Viewer. - Updated PMM UI user loading and feature gating to support anonymous users (new
isAnonymousfield, updated HA/settings enablement, updated API calls).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/docker-compose.yml | Adds developer guidance for mounting/building/swapping Grafana backend locally. |
| ui/apps/pmm/src/utils/testStubs.ts | Updates test user stub to include isAnonymous. |
| ui/apps/pmm/src/types/user.types.ts | Adds isAnonymous to User and GetUserResponse types. |
| ui/apps/pmm/src/hooks/api/useHA.ts | Allows useHaInfo to accept query options (e.g., enabled). |
| ui/apps/pmm/src/contexts/user/user.utils.ts | Plumbs isAnonymous into the derived PMM User. |
| ui/apps/pmm/src/contexts/user/user.provider.tsx | Fetches current user/orgs regardless of login state and synthesizes minimal info/preferences for anonymous users. |
| ui/apps/pmm/src/contexts/settings/settings.provider.tsx | Prevents settings queries from running for anonymous users. |
| ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx | Disables HA info fetching for anonymous users; adjusts service-types fetching behavior. |
| ui/apps/pmm/src/api/user.ts | Switches current-user API calls from Grafana API to PMM-managed /v1/users/current* endpoints. |
| ui/apps/pmm/src/App.tsx | Disables React Query retries globally. |
| ui/Makefile | Adds helper targets to build/swap Grafana backend binary inside the dev container. |
| managed/services/user/current_http.go | New HTTP handler serving /v1/users/current and /v1/users/current/orgs. |
| managed/services/grafana/client_test.go | Adds unit tests for anonymous fallback behavior in Grafana client methods. |
| managed/services/grafana/client.go | Implements anonymous-role resolution/clamping and exposes “current user/orgs” methods with anonymous fallback. |
| managed/services/grafana/auth_server.go | Allows unauthenticated access to the new endpoints and skips LBAC filters for anonymous users. |
| managed/cmd/pmm-managed/main.go | Wires the new current-user HTTP handler into the HTTP/1 server mux. |
| l.Debugf("maybeAddLBACFilters: userID=%d", userID) | ||
| if !s.shallAddLBACFilters(req) { | ||
| l.Debugf("Skipping LBAC filters for non-proxied request.") | ||
| return nil | ||
| } |
There was a problem hiding this comment.
These per-request Debug logs in maybeAddLBACFilters will generate a lot of log volume whenever debug logging is enabled (and they run even for the common no-op path). Consider removing them, lowering to Trace, or logging only when filters are actually added / when an error occurs.
| if err != nil { | ||
| h.l.Errorf("failed to get current user: %v", err) | ||
| rw.WriteHeader(http.StatusUnauthorized) | ||
| _ = json.NewEncoder(rw).Encode(map[string]string{"message": "Unauthorized"}) | ||
| return |
There was a problem hiding this comment.
On error, this endpoint always responds with 401 Unauthorized and a generic body. That will mask real failures (e.g., Grafana unreachable/5xx) as auth problems; consider propagating upstream status codes for 401/403 and returning an appropriate 5xx (e.g., 502) for upstream/internal errors.
| if err != nil { | ||
| h.l.Errorf("failed to get current user orgs: %v", err) | ||
| rw.WriteHeader(http.StatusUnauthorized) | ||
| _ = json.NewEncoder(rw).Encode(map[string]string{"message": "Unauthorized"}) | ||
| return |
There was a problem hiding this comment.
On error, this endpoint always responds with 401 Unauthorized and a generic body. That will mask real failures (e.g., Grafana unreachable/5xx) as auth problems; consider propagating upstream status codes for 401/403 and returning an appropriate 5xx (e.g., 502) for upstream/internal errors.
| func (h *currentHTTPHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||
| authHeaders := make(http.Header) | ||
| for _, k := range []string{"Authorization", "Cookie"} { | ||
| if v := req.Header.Get(k); v != "" { | ||
| authHeaders.Set(k, v) |
There was a problem hiding this comment.
ServeHTTP doesn't validate the HTTP method; non-GET requests to these endpoints will currently be processed the same way. Consider restricting to GET and returning 405 for other methods to keep the API contract tight.
| // NewCurrentHTTPHandler creates handler for current user JSON endpoints. | ||
| func NewCurrentHTTPHandler(c currentUserClient) http.Handler { | ||
| return ¤tHTTPHandler{ | ||
| c: c, | ||
| l: logrus.WithField("component", "user/current-http"), |
There was a problem hiding this comment.
New current-user HTTP endpoints are introduced here, but there are no unit tests covering the handler behavior (routing, method handling, status code mapping, anonymous vs authenticated headers). Since this package already has unit tests, adding coverage for this handler would help prevent regressions.
| # Build/swap/verify Grafana backend via helper targets in ui/Makefile: | ||
| # make -C ui grafana-be-build | ||
| # make -C ui grafana-be-swap | ||
| # make -C ui grafana-be-verify |
There was a problem hiding this comment.
The docker-compose comment references a grafana-be-verify Make target, but ui/Makefile doesn't define it. This makes the instructions misleading; either add the missing target or remove/update the reference here.
| # Build/swap/verify Grafana backend via helper targets in ui/Makefile: | |
| # make -C ui grafana-be-build | |
| # make -C ui grafana-be-swap | |
| # make -C ui grafana-be-verify | |
| # Build/swap Grafana backend via helper targets in ui/Makefile: | |
| # make -C ui grafana-be-build | |
| # make -C ui grafana-be-swap |
|
|
||
| const navTree = useMemo<NavItem[]>(() => { | ||
| const items: NavItem[] = []; | ||
| // provide all service types for anonymous mode |
There was a problem hiding this comment.
The comment says "provide all service types for anonymous mode", but currentServiceTypes is now always derived from serviceTypes?.serviceTypes with an empty-array fallback. Either restore the anonymous fallback behavior or update/remove the comment to match the actual logic.
| // provide all service types for anonymous mode | |
| // use fetched service types, falling back to an empty list while unavailable |
| var ( | ||
| anonymousEnabled bool | ||
| anonymousRole role | ||
| ) | ||
| if !hasAuthorizationHeader(authHeaders) { | ||
| anonymousEnabled, anonymousRole = c.getAnonymousRoleFromSettings(ctx, l) | ||
| } | ||
|
|
||
| // https://grafana.com/docs/http_api/user/#actual-user - works only with Basic Auth | ||
| var m map[string]interface{} | ||
| err := c.do(ctx, http.MethodGet, "/api/user", "", authHeaders, nil, &m) | ||
| if err != nil { | ||
| var cErr *clientError | ||
| if anonymousEnabled && errors.As(err, &cErr) && cErr.Code == http.StatusUnauthorized { | ||
| l.Debugf("Grafana returned 401 for /api/user with no credentials; using anonymous role %q.", anonymousRole.String()) | ||
| return authUser{ | ||
| role: anonymousRole, | ||
| userID: 0, | ||
| }, nil |
There was a problem hiding this comment.
getAuthUser fetches /api/frontend/settings whenever there is no Authorization header, even if /api/user would succeed (e.g., valid session cookie). With the auth cache TTL at 3s, this adds extra Grafana API calls; consider fetching frontend settings only after /api/user returns 401 when deciding to fall back to anonymous.
| var ( | |
| anonymousEnabled bool | |
| anonymousRole role | |
| ) | |
| if !hasAuthorizationHeader(authHeaders) { | |
| anonymousEnabled, anonymousRole = c.getAnonymousRoleFromSettings(ctx, l) | |
| } | |
| // https://grafana.com/docs/http_api/user/#actual-user - works only with Basic Auth | |
| var m map[string]interface{} | |
| err := c.do(ctx, http.MethodGet, "/api/user", "", authHeaders, nil, &m) | |
| if err != nil { | |
| var cErr *clientError | |
| if anonymousEnabled && errors.As(err, &cErr) && cErr.Code == http.StatusUnauthorized { | |
| l.Debugf("Grafana returned 401 for /api/user with no credentials; using anonymous role %q.", anonymousRole.String()) | |
| return authUser{ | |
| role: anonymousRole, | |
| userID: 0, | |
| }, nil | |
| noAuthorizationHeader := !hasAuthorizationHeader(authHeaders) | |
| // https://grafana.com/docs/http_api/user/#actual-user - works only with Basic Auth | |
| var m map[string]interface{} | |
| err := c.do(ctx, http.MethodGet, "/api/user", "", authHeaders, nil, &m) | |
| if err != nil { | |
| var cErr *clientError | |
| if noAuthorizationHeader && errors.As(err, &cErr) && cErr.Code == http.StatusUnauthorized { | |
| anonymousEnabled, anonymousRole := c.getAnonymousRoleFromSettings(ctx, l) | |
| if anonymousEnabled { | |
| l.Debugf("Grafana returned 401 for /api/user with no credentials; using anonymous role %q.", anonymousRole.String()) | |
| return authUser{ | |
| role: anonymousRole, | |
| userID: 0, | |
| }, nil | |
| } |
PMM-14880
Allow org roles to anonymous users
Tied to percona/grafana#886
Note: we fallback to "Viewer" is any other org_role is used on anon mode, due to this deprecation notice:
https://github.com/grafana/grafana/pull/101411/changes