diff --git a/conf/defaults.ini b/conf/defaults.ini index 386ffe58c4e..8cad3c0a891 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -475,6 +475,7 @@ team_ids = allowed_organizations = role_attribute_path = role_attribute_strict = false +allow_assign_grafana_admin = false #################################### GitLab Auth ######################### [auth.gitlab] @@ -490,6 +491,7 @@ allowed_domains = allowed_groups = role_attribute_path = role_attribute_strict = false +allow_assign_grafana_admin = false #################################### Google Auth ######################### [auth.google] @@ -535,6 +537,7 @@ token_url = https://login.microsoftonline.com//oauth2/v2.0/token allowed_domains = allowed_groups = role_attribute_strict = false +allow_assign_grafana_admin = false #################################### Okta OAuth ####################### [auth.okta] @@ -552,6 +555,7 @@ allowed_domains = allowed_groups = role_attribute_path = role_attribute_strict = false +allow_assign_grafana_admin = false #################################### Generic OAuth ####################### [auth.generic_oauth] @@ -585,6 +589,7 @@ tls_client_key = tls_client_ca = use_pkce = false auth_style = +allow_assign_grafana_admin = false #################################### Basic Auth ########################## [auth.basic] diff --git a/conf/sample.ini b/conf/sample.ini index 78387ec2d09..8a60c1758b3 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -473,6 +473,9 @@ ;allowed_domains = ;team_ids = ;allowed_organizations = +;role_attribute_path = +;role_attribute_strict = false +;allow_assign_grafana_admin = false #################################### GitLab Auth ######################### [auth.gitlab] @@ -486,6 +489,9 @@ ;api_url = https://gitlab.com/api/v4 ;allowed_domains = ;allowed_groups = +;role_attribute_path = +;role_attribute_strict = false +;allow_assign_grafana_admin = false #################################### Google Auth ########################## [auth.google] @@ -522,6 +528,7 @@ ;allowed_domains = ;allowed_groups = ;role_attribute_strict = false +;allow_assign_grafana_admin = false #################################### Okta OAuth ####################### [auth.okta] @@ -538,6 +545,7 @@ ;allowed_groups = ;role_attribute_path = ;role_attribute_strict = false +;allow_assign_grafana_admin = false #################################### Generic OAuth ########################## [auth.generic_oauth] @@ -570,6 +578,7 @@ ;tls_client_ca = ;use_pkce = false ;auth_style = +;allow_assign_grafana_admin = false #################################### Basic Auth ########################## [auth.basic] diff --git a/devenv/docker/blocks/auth/oauth/cloak.sql b/devenv/docker/blocks/auth/oauth/cloak.sql index b8f933d4723..06b00843e01 100644 --- a/devenv/docker/blocks/auth/oauth/cloak.sql +++ b/devenv/docker/blocks/auth/oauth/cloak.sql @@ -2218,6 +2218,7 @@ d4b2c483-1dd3-47f6-86bf-42548009918d \N password 74e29604-ff35-42bb-a26d-4d0b81e b8c9b8b4-5943-43fe-9274-d63fd3e4a139 \N password c685749a-645e-4396-b9ee-6eedbfd89d5e 1656420634344 \N {"value":"IAOFzbDfWwzosZc+Z5nFm/i0B4foqmU4Q0EKG34RU3iwlIYUseEB3BoJqLEfM3Rj9oOSryEbCzblWRDS/5Padw==","salt":"7VR1+KwLVRZ6PenxaQoQTA==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 94aeafd3-71a5-4966-b2b6-34a083df6e92 \N password bdce2246-bb51-4f55-bb81-b7b8856225bc 1656425248776 \N {"value":"uD8KlRNocvZwYq1VZUShVp88zEtMUEeQnLYkW8ZvZXDdn1w1EahwnpNWYIc5QewEm3Nnf3DBYlUUrrbMC4XyfQ==","salt":"REwgUSsxRA/sqM5ujSrpcg==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 624725ce-9e36-4501-8bc8-ec39ee6b98d5 \N password 56eff2b3-e36a-4e3e-84a1-361ad312667b 1656428741229 \N {"value":"4UBzDNd3oPxP54/z7ez1Bd3xSfKJBpbE3rQppM3Xg+2bLaLNoU90TPEK+8SWbpMAFBKHz53qPWrZ50MbNgcGSA==","salt":"iTNvn3xr0acn9wqQxJ3d/A==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 +77f9adeb-4bd6-47bd-93d6-49ac90edc731 \N password b8aada79-3fb4-45cd-95d0-c046f3a0113a 1662476251794 \N {"value":"dQJruhADrlLXvwYwd3L2S7ie5FWLGFxJVZm2Eog92xUH2+oahsM52tFvVfsI4wlbAN+XBqMGsfz9rsXeROWvXw==","salt":"64V0IRC+zdOkJ8l4ejfmHA==","additionalParameters":{}} {"hashIterations":27500,"algorithm":"pbkdf2-sha256","additionalParameters":{}} 10 \. @@ -2571,9 +2572,9 @@ b8a4faaf-86d9-43eb-bb18-0eaa654b35a7 ef7f6eac-9fff-44aa-a86c-5125d52acc82 t ${ro c49bddc6-ec92-4caa-bc04-57ba80a92eb9 grafana f ${role_offline-access} offline_access grafana \N grafana 0f3d47bb-002a-4cd0-a502-725f224308a7 grafana f ${role_uma_authorization} uma_authorization grafana \N grafana 60f1b1ea-9059-41ea-acef-573643b24709 grafana f Grafana Organization Administrator admin grafana \N grafana -c029a218-4519-4537-ae12-d8f3c27a0003 grafana f Grafana Server Admin serveradmin grafana \N grafana c9a776f9-2740-435f-a725-4dbcc17a6c91 grafana f Grafana Viewer viewer grafana \N grafana c4c74006-c346-48cf-8cf1-1617e3e1cde1 grafana f Grafana Editor editor grafana \N grafana +c90ad7c8-d14b-46ed-b94d-2de3baa50ff7 grafana f Grafana Server Admin grafanaadmin grafana \N grafana \. @@ -3301,6 +3302,7 @@ COPY public.user_entity (id, email, email_constraint, email_verified, enabled, f c685749a-645e-4396-b9ee-6eedbfd89d5e oauth-admin@example.org oauth-admin@example.org f t \N Admin Oauth grafana oauth-admin 1656418530879 \N 0 56eff2b3-e36a-4e3e-84a1-361ad312667b oauth-editor@example.org oauth-editor@example.org f t \N Editor Oauth grafana oauth-editor 1656418563005 \N 0 bdce2246-bb51-4f55-bb81-b7b8856225bc oauth-viewer@example.org oauth-viewer@example.org f t \N Viewer Oauth grafana oauth-viewer 1656425237046 \N 0 +b8aada79-3fb4-45cd-95d0-c046f3a0113a oauth-grafanaadmin@example.org oauth-grafanaadmin@example.org t t \N Grafanaadmin Oauth grafana oauth-grafanaadmin 1662476222024 \N 0 \. @@ -3376,6 +3378,11 @@ c49bddc6-ec92-4caa-bc04-57ba80a92eb9 bdce2246-bb51-4f55-bb81-b7b8856225bc 0f3d47bb-002a-4cd0-a502-725f224308a7 bdce2246-bb51-4f55-bb81-b7b8856225bc f1311ecb-6a6a-49d6-bb16-5132daf93a64 bdce2246-bb51-4f55-bb81-b7b8856225bc 18a7066b-fe71-410e-9581-69f78347ec29 bdce2246-bb51-4f55-bb81-b7b8856225bc +c49bddc6-ec92-4caa-bc04-57ba80a92eb9 b8aada79-3fb4-45cd-95d0-c046f3a0113a +0f3d47bb-002a-4cd0-a502-725f224308a7 b8aada79-3fb4-45cd-95d0-c046f3a0113a +f1311ecb-6a6a-49d6-bb16-5132daf93a64 b8aada79-3fb4-45cd-95d0-c046f3a0113a +18a7066b-fe71-410e-9581-69f78347ec29 b8aada79-3fb4-45cd-95d0-c046f3a0113a +c90ad7c8-d14b-46ed-b94d-2de3baa50ff7 b8aada79-3fb4-45cd-95d0-c046f3a0113a \. diff --git a/devenv/docker/blocks/auth/oauth/readme.md b/devenv/docker/blocks/auth/oauth/readme.md index c3af2cac344..964a6d29ac4 100644 --- a/devenv/docker/blocks/auth/oauth/readme.md +++ b/devenv/docker/blocks/auth/oauth/readme.md @@ -26,7 +26,8 @@ name_attribute_path = name auth_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/auth token_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/token api_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/userinfo -role_attribute_path = contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer' +role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer' +allow_assign_grafana_admin = true ``` ## Devenv setup jwt auth @@ -112,9 +113,10 @@ docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > - keycloak admin: http://localhost:8087 - keycloak admin login: admin:admin -- grafana oauth viewer login: oauth-viewer:grafana -- grafana oauth editor login: oauth-editor:grafana -- grafana oauth admin login: oauth-admin:grafana +- grafana oauth viewer login: oauth-viewer:grafana +- grafana oauth editor login: oauth-editor:grafana +- grafana oauth admin login: oauth-admin:grafana +- grafana oauth server admin login: oauth-grafanaadmin:grafana # Troubleshooting diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread.md index 7af69ddee48..29053aa016d 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread.md @@ -61,8 +61,8 @@ To enable the Azure AD OAuth2, register your application with Azure AD. "allowedMemberTypes": [ "User" ], - "description": "Grafana admin Users", - "displayName": "Grafana Admin", + "description": "Grafana org admin Users", + "displayName": "Grafana Org Admin", "id": "SOME_UNIQUE_ID", "isEnabled": true, "lang": null, @@ -100,6 +100,30 @@ To enable the Azure AD OAuth2, register your application with Azure AD. 1. Click on **Users and Groups** and add Users/Groups to the Grafana roles by using **Add User**. +### Assign server administrator privileges + +> Available in Grafana v9.2 and later versions. + +If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. +This is useful if you want to grant server administrator privileges to a subset of users. +Grafana also assigns the user the `Admin` role of the default organization. + +The setting `allow_assign_grafana_admin` under `[auth.azuread]` must be set to `true` for this to work. +If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. + +```json +{ + "allowedMemberTypes": ["User"], + "description": "Grafana server admin Users", + "displayName": "Grafana Server Admin", + "id": "SOME_UNIQUE_ID", + "isEnabled": true, + "lang": null, + "origin": "Application", + "value": "GrafanaAdmin" +} +``` + ## Enable Azure AD OAuth in Grafana 1. Add the following to the [Grafana configuration file]({{< relref "../../configure-grafana/#config-file-locations" >}}): @@ -117,6 +141,7 @@ token_url = https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token allowed_domains = allowed_groups = role_attribute_strict = false +allow_assign_grafana_admin = false ``` You can also use these environment variables to configure **client_id** and **client_secret**: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth.md index 4033bfc3942..1141bbf9055 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth.md @@ -296,6 +296,27 @@ Config: role_attribute_path = contains(info.roles[*], 'admin') && 'Admin' || contains(info.roles[*], 'editor') && 'Editor' || 'Viewer' ``` +#### Map server administrator privileges + +> Available in Grafana v9.2 and later versions. + +If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. +This is useful if you want to grant server administrator privileges to a subset of users. +Grafana also assigns the user the `Admin` role of the default organization. + +The setting `allow_assign_grafana_admin` under `[auth.generic_oauth]` must be set to `true` for this to work. +If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. + +```ini +allow_assign_grafana_admin = true +``` + +Example: + +```ini +role_attribute_path = contains(info.roles[*], 'admin') && 'GrafanaAdmin' || contains(info.roles[*], 'editor') && 'Editor' || 'Viewer' +``` + ### Groups mapping > Available in Grafana Enterprise v8.1 and later versions. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github.md index 557ef871282..eed78dcdac5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github.md @@ -130,6 +130,27 @@ role_attribute_path = contains(groups[*], '@github/example-group') && 'Editor' | Note: If a match is found in other fields, teams will be ignored. +#### Map server administrator privileges + +> Available in Grafana v9.2 and later versions. + +If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. +This is useful if you want to grant server administrator privileges to a subset of users. +Grafana also assigns the user the `Admin` role of the default organization. + +The setting `allow_assign_grafana_admin` under `[auth.github]` must be set to `true` for this to work. +If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. + +```ini +allow_assign_grafana_admin = true +``` + +Example: + +```ini +role_attribute_path = [login==octocat] && 'GrafanaAdmin' || 'Viewer' +``` + ### Team Sync (Enterprise only) > Only available in Grafana Enterprise v6.3+ diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab.md index 0638161f0e0..4ef5a47739c 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab.md @@ -58,6 +58,9 @@ auth_url = https://gitlab.com/oauth/authorize token_url = https://gitlab.com/oauth/token api_url = https://gitlab.com/api/v4 allowed_groups = +role_attribute_path = +role_attribute_strict = false +allow_assign_grafana_admin = false ``` You may have to set the `root_url` option of `[server]` for the callback URL to be @@ -102,7 +105,7 @@ characters. Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. Here's a complete example with `allow_sign_up` enabled, with access limited to -the `example` and `foo/bar` groups. The example also promotes all GitLab Admins to Grafana Admins: +the `example` and `foo/bar` groups. The example also promotes all GitLab Admins to Grafana organization admins: ```ini [auth.gitlab] @@ -116,6 +119,8 @@ token_url = https://gitlab.com/oauth/token api_url = https://gitlab.com/api/v4 allowed_groups = example, foo/bar role_attribute_path = is_admin && 'Admin' || 'Viewer' +role_attribute_strict = true +allow_assign_grafana_admin = false ``` ### Map roles @@ -126,7 +131,7 @@ For the path lookup, Grafana uses JSON obtained from querying GitLab's API [`/ap An example Query could look like the following: -```bash +```ini role_attribute_path = is_admin && 'Admin' || 'Viewer' ``` @@ -139,12 +144,33 @@ Groups can also be used to map roles. Group name (lowercased and unique) is used For instance, if you have a group with display name 'Example-Group' you can use the following snippet to ensure those members inherit the role 'Editor'. -```bash +```ini role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer' ``` Note: If a match is found in other fields, groups will be ignored. +#### Map server administrator privileges + +> Available in Grafana v9.2 and later versions. + +If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. +This is useful if you want to grant server administrator privileges to a subset of users. +Grafana also assigns the user the `Admin` role of the default organization. + +The setting `allow_assign_grafana_admin` under `[auth.gitlab]` must be set to `true` for this to work. +If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. + +```ini +allow_assign_grafana_admin = true +``` + +Example: + +```ini +role_attribute_path = is_admin && 'GrafanaAdmin' || 'Viewer' +``` + ### Team Sync (Enterprise only) > Only available in Grafana Enterprise v6.4+ diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta.md index 75e27e598e3..04453993eb5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta.md @@ -77,6 +77,27 @@ Grafana uses JSON obtained from querying the `/userinfo` endpoint for the path l Read about how to [add custom claims](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/add-custom-claim/) to the user info in Okta. Also, check Generic OAuth page for [JMESPath examples]({{< relref "generic-oauth/#jmespath-examples" >}}). +#### Map server administrator privileges + +> Available in Grafana v9.2 and later versions. + +If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. +This is useful if you want to grant server administrator privileges to a subset of users. +Grafana also assigns the user the `Admin` role of the default organization. + +The setting `allow_assign_grafana_admin` under `[auth.okta]` must be set to `true` for this to work. +If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. + +```ini +allow_assign_grafana_admin = true +``` + +Example: + +```ini +role_attribute_path = contains(groups[*], 'admin') && 'GrafanaAdmin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer' +``` + ### Team Sync (Enterprise only) Map your Okta groups to teams in Grafana so that your users will automatically be added to diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index a6161252eee..34ff4ee91cb 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -263,14 +263,15 @@ func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *socia oauthLogger.Debug("Building external user info from OAuth user info") extUser := &models.ExternalUserInfo{ - AuthModule: fmt.Sprintf("oauth_%s", name), - OAuthToken: token, - AuthId: userInfo.Id, - Name: userInfo.Name, - Login: userInfo.Login, - Email: userInfo.Email, - OrgRoles: map[int64]org.RoleType{}, - Groups: userInfo.Groups, + AuthModule: fmt.Sprintf("oauth_%s", name), + OAuthToken: token, + AuthId: userInfo.Id, + Name: userInfo.Name, + Login: userInfo.Login, + Email: userInfo.Email, + OrgRoles: map[int64]org.RoleType{}, + Groups: userInfo.Groups, + IsGrafanaAdmin: userInfo.IsGrafanaAdmin, } if userInfo.Role != "" && !hs.Cfg.OAuthSkipOrgRoleUpdateSync { diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index 4fee59d4a49..880652a7e8b 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -3,7 +3,6 @@ package social import ( "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -18,9 +17,7 @@ import ( type SocialAzureAD struct { *SocialBase - allowedGroups []string - autoAssignOrgRole string - roleAttributeStrict bool + allowedGroups []string } type azureClaims struct { @@ -53,7 +50,7 @@ func (s *SocialAzureAD) Type() int { func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { idToken := token.Extra("id_token") if idToken == nil { - return nil, fmt.Errorf("no id_token found") + return nil, ErrIDTokenNotFound } parsedToken, err := jwt.ParseSigned(idToken.(string)) @@ -68,12 +65,12 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas email := claims.extractEmail() if email == "" { - return nil, errors.New("error getting user info: no email found in access token") + return nil, ErrEmailNotFound } - role := claims.extractRole(s.autoAssignOrgRole, s.roleAttributeStrict) + role, grafanaAdmin := claims.extractRoleAndAdmin(s.autoAssignOrgRole, s.roleAttributeStrict) if role == "" { - return nil, errors.New("user does not have a valid role") + return nil, ErrInvalidBasicRole } logger.Debug("AzureAD OAuth: extracted role", "email", email, "role", role) @@ -87,13 +84,19 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas return nil, errMissingGroupMembership } + var isGrafanaAdmin *bool = nil + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin + } + return &BasicUserInfo{ - Id: claims.ID, - Name: claims.Name, - Email: email, - Login: email, - Role: string(role), - Groups: groups, + Id: claims.ID, + Name: claims.Name, + Email: email, + Login: email, + Role: string(role), + IsGrafanaAdmin: isGrafanaAdmin, + Groups: groups, }, nil } @@ -123,16 +126,18 @@ func (claims *azureClaims) extractEmail() string { return claims.Email } -func (claims *azureClaims) extractRole(autoAssignRole string, strictMode bool) org.RoleType { +// extractRoleAndAdmin extracts the role from the claims and returns the role and whether the user is a Grafana admin. +func (claims *azureClaims) extractRoleAndAdmin(autoAssignRole string, strictMode bool) (org.RoleType, bool) { if len(claims.Roles) == 0 { if strictMode { - return org.RoleType("") + return org.RoleType(""), false } - return org.RoleType(autoAssignRole) + return org.RoleType(autoAssignRole), false } roleOrder := []org.RoleType{ + RoleGrafanaAdmin, org.RoleAdmin, org.RoleEditor, org.RoleViewer, @@ -140,15 +145,19 @@ func (claims *azureClaims) extractRole(autoAssignRole string, strictMode bool) o for _, role := range roleOrder { if found := hasRole(claims.Roles, role); found { - return role + if role == RoleGrafanaAdmin { + return org.RoleAdmin, true + } + + return role, false } } if strictMode { - return org.RoleType("") + return org.RoleType(""), false } - return org.RoleViewer + return org.RoleViewer, false } func hasRole(roles []string, role org.RoleType) bool { @@ -157,6 +166,7 @@ func hasRole(roles []string, role org.RoleType) bool { return true } } + return false } diff --git a/pkg/login/social/azuread_oauth_test.go b/pkg/login/social/azuread_oauth_test.go index a91c3da3516..60d8f75076e 100644 --- a/pkg/login/social/azuread_oauth_test.go +++ b/pkg/login/social/azuread_oauth_test.go @@ -16,12 +16,20 @@ import ( "gopkg.in/square/go-jose.v2/jwt" ) +func trueBoolPtr() *bool { + b := true + return &b +} + +func falseBoolPtr() *bool { + b := false + return &b +} + func TestSocialAzureAD_UserInfo(t *testing.T) { type fields struct { - SocialBase *SocialBase - allowedGroups []string - autoAssignOrgRole string - roleAttributeStrict bool + SocialBase *SocialBase + allowedGroups []string } type args struct { client *http.Client @@ -46,16 +54,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, fields: fields{ - autoAssignOrgRole: "Viewer", + SocialBase: &SocialBase{autoAssignOrgRole: "Viewer"}, }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Viewer", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Viewer", + Groups: []string{}, }, }, { @@ -86,16 +93,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, fields: fields{ - autoAssignOrgRole: "Viewer", + SocialBase: &SocialBase{autoAssignOrgRole: "Viewer"}, }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Viewer", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Viewer", + Groups: []string{}, }, }, { @@ -108,13 +114,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Admin", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + Groups: []string{}, }, }, { @@ -127,13 +132,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Admin", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + Groups: []string{}, }, }, { @@ -146,13 +150,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Viewer", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Viewer", + Groups: []string{}, }, }, { @@ -165,16 +168,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, fields: fields{ - autoAssignOrgRole: "Editor", + SocialBase: &SocialBase{autoAssignOrgRole: "Editor"}, }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Editor", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Editor", + Groups: []string{}, }, }, { @@ -187,13 +189,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Editor", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Editor", + Groups: []string{}, }, }, { @@ -206,13 +207,74 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Admin", - Groups: []string{}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + Groups: []string{}, + }, + }, + { + name: "Grafana Admin but setting is disabled", + fields: fields{SocialBase: &SocialBase{allowAssignGrafanaAdmin: false}}, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"GrafanaAdmin"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + Groups: []string{}, + IsGrafanaAdmin: nil, + }, + }, + { + name: "Editor roles in claim and GrafanaAdminAssignment enabled", + fields: fields{ + SocialBase: newSocialBase("azuread", + &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "")}, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"Editor"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Editor", + Groups: []string{}, + IsGrafanaAdmin: falseBoolPtr(), + }, + }, + { + name: "Grafana Admin and Editor roles in claim", + fields: fields{SocialBase: &SocialBase{allowAssignGrafanaAdmin: true}}, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"GrafanaAdmin", "Editor"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + Groups: []string{}, + IsGrafanaAdmin: trueBoolPtr(), }, }, { @@ -234,8 +296,8 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Error if user is a member of allowed_groups", fields: fields{ - allowedGroups: []string{"foo", "bar"}, - autoAssignOrgRole: "Viewer", + allowedGroups: []string{"foo", "bar"}, + SocialBase: &SocialBase{autoAssignOrgRole: "Viewer"}, }, claims: &azureClaims{ Email: "me@example.com", @@ -246,13 +308,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, want: &BasicUserInfo{ - Id: "1234", - Name: "My Name", - Email: "me@example.com", - Login: "me@example.com", - Company: "", - Role: "Viewer", - Groups: []string{"foo"}, + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Viewer", + Groups: []string{"foo"}, }, }, { @@ -283,7 +344,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Fetch empty role when strict attribute role is true and no match", fields: fields{ - roleAttributeStrict: true, + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, ""), }, claims: &azureClaims{ Email: "me@example.com", @@ -299,7 +360,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Fetch empty role when strict attribute role is true and no role claims returned", fields: fields{ - roleAttributeStrict: true, + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, ""), }, claims: &azureClaims{ Email: "me@example.com", @@ -313,13 +374,16 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { wantErr: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &SocialAzureAD{ - SocialBase: tt.fields.SocialBase, - allowedGroups: tt.fields.allowedGroups, - autoAssignOrgRole: tt.fields.autoAssignOrgRole, - roleAttributeStrict: tt.fields.roleAttributeStrict, + SocialBase: tt.fields.SocialBase, + allowedGroups: tt.fields.allowedGroups, + } + + if tt.fields.SocialBase == nil { + s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "") } key := []byte("secret") @@ -357,14 +421,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { } } raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize() - if err != nil { - t.Error(err) - } + require.NoError(t, err) } else { raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize() - if err != nil { - t.Error(err) - } + require.NoError(t, err) } token := &oauth2.Token{ diff --git a/pkg/login/social/errors.go b/pkg/login/social/errors.go new file mode 100644 index 00000000000..2624050a717 --- /dev/null +++ b/pkg/login/social/errors.go @@ -0,0 +1,9 @@ +package social + +import "errors" + +var ( + ErrIDTokenNotFound = errors.New("id_token not found") + ErrInvalidBasicRole = errors.New("user does not have a valid basic role") + ErrEmailNotFound = errors.New("error getting user info: no email found in access token") +) diff --git a/pkg/login/social/generic_oauth.go b/pkg/login/social/generic_oauth.go index cb682fcc0e8..819c99b862a 100644 --- a/pkg/login/social/generic_oauth.go +++ b/pkg/login/social/generic_oauth.go @@ -14,7 +14,6 @@ import ( "strconv" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/org" "golang.org/x/oauth2" ) @@ -27,8 +26,6 @@ type SocialGenericOAuth struct { emailAttributePath string loginAttributePath string nameAttributePath string - roleAttributePath string - roleAttributeStrict bool groupsAttributePath string idTokenAttributeName string teamIdsAttributePath string @@ -147,12 +144,18 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) } if userInfo.Role == "" { - role, err := s.extractRole(data) - if err != nil { - s.log.Warn("Failed to extract role", "error", err) - } else if role != "" { + role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, []string{}) + if role != "" { + if s.roleAttributeStrict && !role.IsValid() { + return nil, ErrInvalidBasicRole + } + s.log.Debug("Setting user info role from extracted role") - userInfo.Role = role + + userInfo.Role = string(role) + if s.allowAssignGrafanaAdmin { + userInfo.IsGrafanaAdmin = &grafanaAdmin + } } } @@ -181,10 +184,6 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) userInfo.Login = userInfo.Email } - if s.roleAttributeStrict && !org.RoleType(userInfo.Role).IsValid() { - return nil, errors.New("invalid role") - } - if !s.IsTeamMember(client) { return nil, errors.New("user not a member of one of the required teams") } @@ -355,19 +354,6 @@ func (s *SocialGenericOAuth) extractUserName(data *UserInfoJson) string { return "" } -func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) (string, error) { - if s.roleAttributePath == "" { - return "", nil - } - - role, err := s.searchJSONForStringAttr(s.roleAttributePath, data.rawJSON) - - if err != nil { - return "", err - } - return role, nil -} - func (s *SocialGenericOAuth) extractGroups(data *UserInfoJson) ([]string, error) { if s.groupsAttributePath == "" { return []string{}, nil diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index bb7426f9f86..3c5e50fe5dc 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -245,12 +245,14 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { } tests := []struct { - Name string - ResponseBody interface{} - OAuth2Extra interface{} - RoleAttributePath string - ExpectedEmail string - ExpectedRole string + Name string + AllowAssignGrafanaAdmin bool + ResponseBody interface{} + OAuth2Extra interface{} + RoleAttributePath string + ExpectedEmail string + ExpectedRole string + ExpectedGrafanaAdmin *bool }{ { Name: "Given a valid id_token, a valid role path, no API response, use id_token", @@ -330,6 +332,38 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedEmail: "john.doe@example.com", ExpectedRole: "Admin", }, + { + Name: "Given a valid id_token and AssignGrafanaAdmin is unchecked, don't grant Server Admin", + AllowAssignGrafanaAdmin: false, + OAuth2Extra: map[string]interface{}{ + // { "role": "GrafanaAdmin", "email": "john.doe@example.com" } + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI", + }, + ResponseBody: map[string]interface{}{ + "role": "FromResponse", + "email": "from_response@example.com", + }, + RoleAttributePath: "role", + ExpectedEmail: "john.doe@example.com", + ExpectedRole: "Admin", + ExpectedGrafanaAdmin: nil, + }, + { + Name: "Given a valid id_token and AssignGrafanaAdmin is checked, grant Server Admin", + AllowAssignGrafanaAdmin: true, + OAuth2Extra: map[string]interface{}{ + // { "role": "GrafanaAdmin", "email": "john.doe@example.com" } + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI", + }, + ResponseBody: map[string]interface{}{ + "role": "FromResponse", + "email": "from_response@example.com", + }, + RoleAttributePath: "role", + ExpectedEmail: "john.doe@example.com", + ExpectedRole: "Admin", + ExpectedGrafanaAdmin: trueBoolPtr(), + }, { Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token", OAuth2Extra: map[string]interface{}{ @@ -368,7 +402,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { }, RoleAttributePath: "role", ExpectedEmail: "john.doe@example.com", - ExpectedRole: "FromResponse", + ExpectedRole: "Fromresponse", }, { Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role", @@ -416,6 +450,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { for _, test := range tests { provider.roleAttributePath = test.RoleAttributePath + provider.allowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin + t.Run(test.Name, func(t *testing.T) { body, err := json.Marshal(test.ResponseBody) require.NoError(t, err) @@ -439,6 +475,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { require.Equal(t, test.ExpectedEmail, actualResult.Email) require.Equal(t, test.ExpectedEmail, actualResult.Login) require.Equal(t, test.ExpectedRole, actualResult.Role) + require.Equal(t, test.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin) }) } }) diff --git a/pkg/login/social/github_oauth.go b/pkg/login/social/github_oauth.go index a44145b36e6..399072f25c0 100644 --- a/pkg/login/social/github_oauth.go +++ b/pkg/login/social/github_oauth.go @@ -201,21 +201,24 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi teams := convertToGroupList(teamMemberships) - role, err := s.extractRole(response.Body, teams) - if err != nil { - s.log.Error("Failed to extract role", "error", err) - } + role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, teams) if s.roleAttributeStrict && !role.IsValid() { - return nil, errors.New("invalid role") + return nil, ErrInvalidBasicRole + } + + var isGrafanaAdmin *bool = nil + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin } userInfo := &BasicUserInfo{ - Name: data.Login, - Login: data.Login, - Id: fmt.Sprintf("%d", data.Id), - Email: data.Email, - Role: string(role), - Groups: teams, + Name: data.Login, + Login: data.Login, + Id: fmt.Sprintf("%d", data.Id), + Email: data.Email, + Role: string(role), + Groups: teams, + IsGrafanaAdmin: isGrafanaAdmin, } if data.Name != "" { userInfo.Name = data.Name diff --git a/pkg/login/social/github_oauth_test.go b/pkg/login/social/github_oauth_test.go index 305fa125ccb..bd31ba18b8e 100644 --- a/pkg/login/social/github_oauth_test.go +++ b/pkg/login/social/github_oauth_test.go @@ -127,13 +127,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) { autoAssignOrgRole: "", roleAttributePath: "", want: &BasicUserInfo{ - Id: "1", - Name: "monalisa octocat", - Email: "octocat@github.com", - Login: "octocat", - Company: "", - Role: "", - Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, + Id: "1", + Name: "monalisa octocat", + Email: "octocat@github.com", + Login: "octocat", + Role: "", + Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, }, }, { @@ -143,13 +142,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) { autoAssignOrgRole: "Editor", userTeamsRawJSON: testGHUserTeamsJSON, want: &BasicUserInfo{ - Id: "1", - Name: "monalisa octocat", - Email: "octocat@github.com", - Login: "octocat", - Company: "", - Role: "Admin", - Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, + Id: "1", + Name: "monalisa octocat", + Email: "octocat@github.com", + Login: "octocat", + Role: "Admin", + Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, }, }, { @@ -159,13 +157,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) { autoAssignOrgRole: "Editor", userTeamsRawJSON: testGHUserTeamsJSON, want: &BasicUserInfo{ - Id: "1", - Name: "monalisa octocat", - Email: "octocat@github.com", - Login: "octocat", - Company: "", - Role: "Editor", - Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, + Id: "1", + Name: "monalisa octocat", + Email: "octocat@github.com", + Login: "octocat", + Role: "Editor", + Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, }, }, { @@ -175,13 +172,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) { autoAssignOrgRole: "Editor", userTeamsRawJSON: testGHUserTeamsJSON, want: &BasicUserInfo{ - Id: "1", - Name: "monalisa octocat", - Email: "octocat@github.com", - Login: "octocat", - Company: "", - Role: "Editor", - Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, + Id: "1", + Name: "monalisa octocat", + Email: "octocat@github.com", + Login: "octocat", + Role: "Editor", + Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, }, }, } diff --git a/pkg/login/social/gitlab_oauth.go b/pkg/login/social/gitlab_oauth.go index 326ac0b06c5..0cda9623fd1 100644 --- a/pkg/login/social/gitlab_oauth.go +++ b/pkg/login/social/gitlab_oauth.go @@ -2,7 +2,6 @@ package social import ( "encoding/json" - "errors" "fmt" "net/http" "regexp" @@ -114,21 +113,24 @@ func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*Basi groups := s.GetGroups(client) - role, err := s.extractRole(response.Body, groups) - if err != nil { - s.log.Error("Failed to extract role", "error", err) - } + role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, groups) if s.roleAttributeStrict && !role.IsValid() { - return nil, errors.New("invalid role") + return nil, ErrInvalidBasicRole + } + + var isGrafanaAdmin *bool = nil + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin } userInfo := &BasicUserInfo{ - Id: fmt.Sprintf("%d", data.Id), - Name: data.Name, - Login: data.Username, - Email: data.Email, - Groups: groups, - Role: string(role), + Id: fmt.Sprintf("%d", data.Id), + Name: data.Name, + Login: data.Username, + Email: data.Email, + Groups: groups, + Role: string(role), + IsGrafanaAdmin: isGrafanaAdmin, } if !s.IsGroupMember(groups) { diff --git a/pkg/login/social/okta_oauth.go b/pkg/login/social/okta_oauth.go index ab5ec9a9e5d..770b06858d3 100644 --- a/pkg/login/social/okta_oauth.go +++ b/pkg/login/social/okta_oauth.go @@ -7,17 +7,14 @@ import ( "net/http" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/org" "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2/jwt" ) type SocialOkta struct { *SocialBase - apiUrl string - allowedGroups []string - roleAttributePath string - roleAttributeStrict bool + apiUrl string + allowedGroups []string } type OktaUserInfoJson struct { @@ -78,26 +75,29 @@ func (s *SocialOkta) UserInfo(client *http.Client, token *oauth2.Token) (*BasicU return nil, err } - role, err := s.extractRole(&data) - if err != nil { - s.log.Error("Failed to extract role", "error", err) - } - if s.roleAttributeStrict && !org.RoleType(role).IsValid() { - return nil, errors.New("invalid role") - } - groups := s.GetGroups(&data) if !s.IsGroupMember(groups) { return nil, errMissingGroupMembership } + role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, groups) + if s.roleAttributeStrict && !role.IsValid() { + return nil, ErrInvalidBasicRole + } + + var isGrafanaAdmin *bool = nil + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin + } + return &BasicUserInfo{ - Id: claims.ID, - Name: claims.Name, - Email: email, - Login: email, - Role: role, - Groups: groups, + Id: claims.ID, + Name: claims.Name, + Email: email, + Login: email, + Role: string(role), + IsGrafanaAdmin: isGrafanaAdmin, + Groups: groups, }, nil } @@ -120,18 +120,6 @@ func (s *SocialOkta) extractAPI(data *OktaUserInfoJson, client *http.Client) err return nil } -func (s *SocialOkta) extractRole(data *OktaUserInfoJson) (string, error) { - if s.roleAttributePath == "" { - return "", nil - } - - role, err := s.searchJSONForStringAttr(s.roleAttributePath, data.rawJSON) - if err != nil { - return "", err - } - return role, nil -} - func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string { groups := make([]string, 0) if len(data.Groups) > 0 { diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index fb28128b665..3b710e9cc07 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -12,6 +12,8 @@ import ( "context" "golang.org/x/oauth2" + "golang.org/x/text/cases" + "golang.org/x/text/language" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" @@ -31,28 +33,29 @@ type SocialService struct { } type OAuthInfo struct { - ClientId, ClientSecret string - Scopes []string - AuthUrl, TokenUrl string - Enabled bool - EmailAttributeName string - EmailAttributePath string - RoleAttributePath string - RoleAttributeStrict bool - GroupsAttributePath string - TeamIdsAttributePath string - AllowedDomains []string - HostedDomain string - ApiUrl string - TeamsUrl string - AllowSignup bool - Name string - Icon string - TlsClientCert string - TlsClientKey string - TlsClientCa string - TlsSkipVerify bool - UsePKCE bool + ClientId, ClientSecret string + Scopes []string + AuthUrl, TokenUrl string + Enabled bool + EmailAttributeName string + EmailAttributePath string + RoleAttributePath string + RoleAttributeStrict bool + GroupsAttributePath string + TeamIdsAttributePath string + AllowedDomains []string + AllowAssignGrafanaAdmin bool + HostedDomain string + ApiUrl string + TeamsUrl string + AllowSignup bool + Name string + Icon string + TlsClientCert string + TlsClientKey string + TlsClientCa string + TlsSkipVerify bool + UsePKCE bool } func ProvideService(cfg *setting.Cfg) *SocialService { @@ -66,30 +69,31 @@ func ProvideService(cfg *setting.Cfg) *SocialService { sec := cfg.Raw.Section("auth." + name) info := &OAuthInfo{ - ClientId: sec.Key("client_id").String(), - ClientSecret: sec.Key("client_secret").String(), - Scopes: util.SplitString(sec.Key("scopes").String()), - AuthUrl: sec.Key("auth_url").String(), - TokenUrl: sec.Key("token_url").String(), - ApiUrl: sec.Key("api_url").String(), - TeamsUrl: sec.Key("teams_url").String(), - Enabled: sec.Key("enabled").MustBool(), - EmailAttributeName: sec.Key("email_attribute_name").String(), - EmailAttributePath: sec.Key("email_attribute_path").String(), - RoleAttributePath: sec.Key("role_attribute_path").String(), - RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(), - GroupsAttributePath: sec.Key("groups_attribute_path").String(), - TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(), - AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), - HostedDomain: sec.Key("hosted_domain").String(), - AllowSignup: sec.Key("allow_sign_up").MustBool(), - Name: sec.Key("name").MustString(name), - Icon: sec.Key("icon").String(), - TlsClientCert: sec.Key("tls_client_cert").String(), - TlsClientKey: sec.Key("tls_client_key").String(), - TlsClientCa: sec.Key("tls_client_ca").String(), - TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), - UsePKCE: sec.Key("use_pkce").MustBool(), + ClientId: sec.Key("client_id").String(), + ClientSecret: sec.Key("client_secret").String(), + Scopes: util.SplitString(sec.Key("scopes").String()), + AuthUrl: sec.Key("auth_url").String(), + TokenUrl: sec.Key("token_url").String(), + ApiUrl: sec.Key("api_url").String(), + TeamsUrl: sec.Key("teams_url").String(), + Enabled: sec.Key("enabled").MustBool(), + EmailAttributeName: sec.Key("email_attribute_name").String(), + EmailAttributePath: sec.Key("email_attribute_path").String(), + RoleAttributePath: sec.Key("role_attribute_path").String(), + RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(), + GroupsAttributePath: sec.Key("groups_attribute_path").String(), + TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(), + AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), + HostedDomain: sec.Key("hosted_domain").String(), + AllowSignup: sec.Key("allow_sign_up").MustBool(), + Name: sec.Key("name").MustString(name), + Icon: sec.Key("icon").String(), + TlsClientCert: sec.Key("tls_client_cert").String(), + TlsClientKey: sec.Key("tls_client_key").String(), + TlsClientCa: sec.Key("tls_client_ca").String(), + TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), + UsePKCE: sec.Key("use_pkce").MustBool(), + AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false), } // when empty_scopes parameter exists and is true, overwrite scope with empty value @@ -163,21 +167,17 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // AzureAD. if name == "azuread" { ss.socialMap["azuread"] = &SocialAzureAD{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole), - allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), - autoAssignOrgRole: cfg.AutoAssignOrgRole, - roleAttributeStrict: info.RoleAttributeStrict, + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole), + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), } } // Okta if name == "okta" { ss.socialMap["okta"] = &SocialOkta{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole), - apiUrl: info.ApiUrl, - allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), - roleAttributePath: info.RoleAttributePath, - roleAttributeStrict: info.RoleAttributeStrict, + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole), + apiUrl: info.ApiUrl, + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), } } @@ -190,8 +190,6 @@ func ProvideService(cfg *setting.Cfg) *SocialService { emailAttributeName: info.EmailAttributeName, emailAttributePath: info.EmailAttributePath, nameAttributePath: sec.Key("name_attribute_path").String(), - roleAttributePath: info.RoleAttributePath, - roleAttributeStrict: info.RoleAttributeStrict, groupsAttributePath: info.GroupsAttributePath, loginAttributePath: sec.Key("login_attribute_path").String(), idTokenAttributeName: sec.Key("id_token_attribute_name").String(), @@ -226,18 +224,18 @@ func ProvideService(cfg *setting.Cfg) *SocialService { } type BasicUserInfo struct { - Id string - Name string - Email string - Login string - Company string - Role string - Groups []string + Id string + Name string + Email string + Login string + Role string + IsGrafanaAdmin *bool // nil will avoid overriding user's set server admin setting + Groups []string } func (b *BasicUserInfo) String() string { - return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Company: %s, Role: %s, Groups: %v", - b.Id, b.Name, b.Email, b.Login, b.Company, b.Role, b.Groups) + return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Role: %s, Groups: %v", + b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups) } type SocialConnector interface { @@ -254,9 +252,10 @@ type SocialConnector interface { type SocialBase struct { *oauth2.Config - log log.Logger - allowSignup bool - allowedDomains []string + log log.Logger + allowSignup bool + allowAssignGrafanaAdmin bool + allowedDomains []string roleAttributePath string roleAttributeStrict bool @@ -272,7 +271,8 @@ func (e Error) Error() string { } const ( - grafanaCom = "grafana_com" + grafanaCom = "grafana_com" + RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces ) var ( @@ -297,13 +297,14 @@ func newSocialBase(name string, logger := log.New("oauth." + name) return &SocialBase{ - Config: config, - log: logger, - allowSignup: info.AllowSignup, - allowedDomains: info.AllowedDomains, - autoAssignOrgRole: autoAssignOrgRole, - roleAttributePath: info.RoleAttributePath, - roleAttributeStrict: info.RoleAttributeStrict, + Config: config, + log: logger, + allowSignup: info.AllowSignup, + allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin, + allowedDomains: info.AllowedDomains, + autoAssignOrgRole: autoAssignOrgRole, + roleAttributePath: info.RoleAttributePath, + roleAttributeStrict: info.RoleAttributeStrict, } } @@ -311,28 +312,38 @@ type groupStruct struct { Groups []string `json:"groups"` } -func (s *SocialBase) extractRole(rawJSON []byte, groups []string) (org.RoleType, error) { +func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string) (org.RoleType, bool) { if s.roleAttributePath == "" { if s.autoAssignOrgRole != "" { - return org.RoleType(s.autoAssignOrgRole), nil + return org.RoleType(s.autoAssignOrgRole), false } - return "", nil + return "", false } role, err := s.searchJSONForStringAttr(s.roleAttributePath, rawJSON) if err == nil && role != "" { - return org.RoleType(role), nil + return getRoleFromSearch(role) } if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil { - if role, err := s.searchJSONForStringAttr( - s.roleAttributePath, groupBytes); err == nil && role != "" { - return org.RoleType(role), nil + role, err := s.searchJSONForStringAttr(s.roleAttributePath, groupBytes) + if err == nil && role != "" { + return getRoleFromSearch(role) } } - return "", nil + return "", false +} + +// match grafana admin role and translate to org role and bool. +// treat the JSON search result to ensure correct casing. +func getRoleFromSearch(role string) (org.RoleType, bool) { + if strings.EqualFold(role, RoleGrafanaAdmin) { + return org.RoleAdmin, true + } + + return org.RoleType(cases.Title(language.Und).String(role)), false } // GetOAuthProviders returns available oauth providers and if they're enabled or not