-
Notifications
You must be signed in to change notification settings - Fork 200
PMM-14880 anonymous role #5170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: PMM-14880-rta-pmm-demo
Are you sure you want to change the base?
PMM-14880 anonymous role #5170
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -168,6 +168,30 @@ type authUser struct { | |
| userID int | ||
| } | ||
|
|
||
| // CurrentUser represents Grafana user payload. | ||
| type CurrentUser struct { | ||
| ID int `json:"id"` | ||
| Email string `json:"email"` | ||
| Name string `json:"name"` | ||
| Login string `json:"login"` | ||
| CreatedAt string `json:"createdAt"` | ||
| OrgID int `json:"orgId"` | ||
| IsAnonymous bool `json:"isAnonymous"` | ||
| IsDisabled bool `json:"isDisabled"` | ||
| IsExternal bool `json:"isExternal"` | ||
| IsExtarnallySynced bool `json:"isExtarnallySynced"` | ||
| IsGrafanaAdmin bool `json:"isGrafanaAdmin"` | ||
| IsGrafanaAdminExternallySynced bool `json:"isGrafanaAdminExternallySynced"` | ||
| Theme string `json:"theme"` | ||
| } | ||
|
|
||
| // CurrentUserOrg represents Grafana org payload. | ||
| type CurrentUserOrg struct { | ||
| OrgID int `json:"orgId"` | ||
| Name string `json:"name"` | ||
| Role string `json:"role"` | ||
| } | ||
|
|
||
| // role defines Grafana user role within the organization | ||
| // (except grafanaAdmin that is a global flag that is more important than any other role). | ||
| // Role with more permissions has larger numerical value: viewer < editor, admin < grafanaAdmin, etc. | ||
|
|
@@ -242,10 +266,26 @@ func (c *Client) getAuthUser(ctx context.Context, authHeaders http.Header, l *lo | |
| }, nil | ||
| } | ||
|
|
||
| var ( | ||
| anonymousEnabled bool | ||
| anonymousRole role | ||
| ) | ||
| if authHeaders.Get("Authorization") == "" && authHeaders.Get("Cookie") == "" { | ||
| 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(errors.Cause(err), &cErr) && cErr.Code == http.StatusUnauthorized { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like |
||
| l.Debugf("Grafana returned 401 for /api/user with no credentials; using anonymous role %q.", anonymousRole.String()) | ||
| return authUser{ | ||
| role: anonymousRole, | ||
| userID: 0, | ||
| }, nil | ||
| } | ||
| return emptyUser, err | ||
| } | ||
|
|
||
|
|
@@ -302,6 +342,161 @@ func (c *Client) convertRole(role string) role { | |
| } | ||
| } | ||
|
|
||
| type frontendUserSettings struct { | ||
| OrgRole string `json:"orgRole"` | ||
| } | ||
|
|
||
| type frontendSettings struct { | ||
| AnonymousEnabled bool `json:"anonymousEnabled"` | ||
| AnonymousOrgRole string `json:"anonymousOrgRole"` | ||
| User frontendUserSettings `json:"user"` | ||
| } | ||
|
|
||
| type frontendUserSettingsFull struct { | ||
| OrgRole string `json:"orgRole"` | ||
| OrgID int `json:"orgId"` | ||
| OrgName string `json:"orgName"` | ||
| } | ||
|
|
||
| type frontendSettingsFull struct { | ||
| AnonymousEnabled bool `json:"anonymousEnabled"` | ||
| AnonymousOrgRole string `json:"anonymousOrgRole"` | ||
| User frontendUserSettingsFull `json:"user"` | ||
| } | ||
|
|
||
| func (c *Client) getAnonymousRoleFromSettings(ctx context.Context, l *logrus.Entry) (bool, role) { | ||
| var settings frontendSettings | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in this method the result from API endpoint |
||
| if err := c.do(ctx, http.MethodGet, "/api/frontend/settings", "", nil, nil, &settings); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is a separate method introduced |
||
| return false, none | ||
| } | ||
|
|
||
| if !settings.AnonymousEnabled { | ||
| return false, none | ||
| } | ||
|
|
||
| // orgRole from frontend user state is the effective role for anonymous access. | ||
| parsedRole := c.convertRole(settings.User.OrgRole) | ||
| if parsedRole == none { | ||
| parsedRole = c.convertRole(settings.AnonymousOrgRole) | ||
| } | ||
| l.Debugf("Grafana anonymous mode is enabled with role %q.", parsedRole.String()) | ||
| return true, parsedRole | ||
| } | ||
|
|
||
| func (c *Client) getFrontendSettings(ctx context.Context) (frontendSettingsFull, error) { | ||
| var settings frontendSettingsFull | ||
| if err := c.do(ctx, http.MethodGet, "/api/frontend/settings", "", nil, nil, &settings); err != nil { | ||
| return frontendSettingsFull{}, err | ||
| } | ||
|
|
||
| return settings, nil | ||
| } | ||
|
|
||
| func hasAuthHeaders(authHeaders http.Header) bool { | ||
| return authHeaders.Get("Authorization") != "" || authHeaders.Get("Cookie") != "" | ||
| } | ||
|
|
||
| func (c *Client) resolveAnonymousRole(settings frontendSettingsFull) string { | ||
| if settings.User.OrgRole != "" { | ||
| return settings.User.OrgRole | ||
| } | ||
| if settings.AnonymousOrgRole != "" { | ||
| return settings.AnonymousOrgRole | ||
| } | ||
| return none.String() | ||
| } | ||
|
|
||
| // GetCurrentUser returns current Grafana user. | ||
| // If anonymous mode is enabled and no auth headers are present, it returns | ||
| // a synthetic anonymous user when /api/user responds with 401. | ||
| func (c *Client) GetCurrentUser(ctx context.Context, authHeaders http.Header) (CurrentUser, error) { | ||
| var user CurrentUser | ||
| err := c.do(ctx, http.MethodGet, "/api/user", "", authHeaders, nil, &user) | ||
| if err == nil { | ||
| return user, nil | ||
| } | ||
|
|
||
| var cErr *clientError | ||
| if !errors.As(errors.Cause(err), &cErr) || cErr.Code != http.StatusUnauthorized || hasAuthHeaders(authHeaders) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
because |
||
| return CurrentUser{}, err | ||
| } | ||
|
|
||
| settings, settingsErr := c.getFrontendSettings(ctx) | ||
| if settingsErr != nil || !settings.AnonymousEnabled { | ||
| return CurrentUser{}, err | ||
| } | ||
| if c.resolveAnonymousRole(settings) == none.String() { | ||
| // Anonymous mode is enabled but role is not configured. | ||
| // Return empty payload instead of Unauthorized. | ||
| return CurrentUser{}, nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is better to return an |
||
| } | ||
|
|
||
| orgID := settings.User.OrgID | ||
| if orgID == 0 { | ||
| orgID = 1 | ||
| } | ||
|
|
||
| return CurrentUser{ | ||
| ID: 0, | ||
| Email: "", | ||
| Name: "Anonymous", | ||
| Login: "anonymous", | ||
| CreatedAt: "", | ||
| OrgID: orgID, | ||
| IsAnonymous: true, | ||
| IsDisabled: false, | ||
| IsExternal: false, | ||
| IsExtarnallySynced: false, | ||
| IsGrafanaAdmin: false, | ||
| IsGrafanaAdminExternallySynced: false, | ||
| Theme: "", | ||
|
Comment on lines
+447
to
+452
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure these fields are required here |
||
| }, nil | ||
| } | ||
|
|
||
| // GetCurrentUserOrgs returns current Grafana user organizations. | ||
| // If anonymous mode is enabled and no auth headers are present, it returns | ||
| // a synthetic org list when /api/user/orgs responds with 401. | ||
| func (c *Client) GetCurrentUserOrgs(ctx context.Context, authHeaders http.Header) ([]CurrentUserOrg, error) { | ||
| var orgs []CurrentUserOrg | ||
| err := c.do(ctx, http.MethodGet, "/api/user/orgs", "", authHeaders, nil, &orgs) | ||
| if err == nil { | ||
| return orgs, nil | ||
| } | ||
|
|
||
| var cErr *clientError | ||
| if !errors.As(errors.Cause(err), &cErr) || cErr.Code != http.StatusUnauthorized || hasAuthHeaders(authHeaders) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the same with |
||
| return nil, err | ||
| } | ||
|
|
||
| settings, settingsErr := c.getFrontendSettings(ctx) | ||
| if settingsErr != nil || !settings.AnonymousEnabled { | ||
| return nil, err | ||
| } | ||
| role := c.resolveAnonymousRole(settings) | ||
| if role == none.String() { | ||
| // Anonymous mode is enabled but role is not configured. | ||
| // Return empty payload instead of Unauthorized. | ||
| return []CurrentUserOrg{}, nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the same regarding error as in |
||
| } | ||
|
|
||
| orgID := settings.User.OrgID | ||
| if orgID == 0 { | ||
| orgID = 1 | ||
| } | ||
| orgName := settings.User.OrgName | ||
| if orgName == "" { | ||
| orgName = "Main Org." | ||
| } | ||
|
|
||
| return []CurrentUserOrg{ | ||
| { | ||
| OrgID: orgID, | ||
| Name: orgName, | ||
| Role: role, | ||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
| func (c *Client) getRoleForServiceToken(ctx context.Context, token string) (role, error) { | ||
| header := http.Header{} | ||
| header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are all fields needed in these structs