OAuth: Allow assigning Server Admin (#54780)

* extract errors to errors file

* implement oauth server admin assignment

* add server admin tests

* deduplicate autoAssignOrgRole

* deduplicate strict setting

* deduplicate strict setting

* add support for generic oauth

* add role attribute strict support for generic oauth

* add support for github/gitlab

* assignGrafanaAdmin option is here to stay

* unify similar errors

* add config option

* add okta server admin mapping

* remove never used Company attribute

* unify generic oauth role extract with other methods

* case insensitive role match as in azure

* add ini settings

* add server admin to devenv

* remove duplicate fields

* add documentation to oauth

* fix titlecase test

* implement doc feedback
This commit is contained in:
Jo 2022-09-08 12:11:00 +02:00 committed by GitHub
parent 1353177e15
commit ef245874da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 561 additions and 321 deletions

View File

@ -475,6 +475,7 @@ team_ids =
allowed_organizations = allowed_organizations =
role_attribute_path = role_attribute_path =
role_attribute_strict = false role_attribute_strict = false
allow_assign_grafana_admin = false
#################################### GitLab Auth ######################### #################################### GitLab Auth #########################
[auth.gitlab] [auth.gitlab]
@ -490,6 +491,7 @@ allowed_domains =
allowed_groups = allowed_groups =
role_attribute_path = role_attribute_path =
role_attribute_strict = false role_attribute_strict = false
allow_assign_grafana_admin = false
#################################### Google Auth ######################### #################################### Google Auth #########################
[auth.google] [auth.google]
@ -535,6 +537,7 @@ token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
allowed_domains = allowed_domains =
allowed_groups = allowed_groups =
role_attribute_strict = false role_attribute_strict = false
allow_assign_grafana_admin = false
#################################### Okta OAuth ####################### #################################### Okta OAuth #######################
[auth.okta] [auth.okta]
@ -552,6 +555,7 @@ allowed_domains =
allowed_groups = allowed_groups =
role_attribute_path = role_attribute_path =
role_attribute_strict = false role_attribute_strict = false
allow_assign_grafana_admin = false
#################################### Generic OAuth ####################### #################################### Generic OAuth #######################
[auth.generic_oauth] [auth.generic_oauth]
@ -585,6 +589,7 @@ tls_client_key =
tls_client_ca = tls_client_ca =
use_pkce = false use_pkce = false
auth_style = auth_style =
allow_assign_grafana_admin = false
#################################### Basic Auth ########################## #################################### Basic Auth ##########################
[auth.basic] [auth.basic]

View File

@ -473,6 +473,9 @@
;allowed_domains = ;allowed_domains =
;team_ids = ;team_ids =
;allowed_organizations = ;allowed_organizations =
;role_attribute_path =
;role_attribute_strict = false
;allow_assign_grafana_admin = false
#################################### GitLab Auth ######################### #################################### GitLab Auth #########################
[auth.gitlab] [auth.gitlab]
@ -486,6 +489,9 @@
;api_url = https://gitlab.com/api/v4 ;api_url = https://gitlab.com/api/v4
;allowed_domains = ;allowed_domains =
;allowed_groups = ;allowed_groups =
;role_attribute_path =
;role_attribute_strict = false
;allow_assign_grafana_admin = false
#################################### Google Auth ########################## #################################### Google Auth ##########################
[auth.google] [auth.google]
@ -522,6 +528,7 @@
;allowed_domains = ;allowed_domains =
;allowed_groups = ;allowed_groups =
;role_attribute_strict = false ;role_attribute_strict = false
;allow_assign_grafana_admin = false
#################################### Okta OAuth ####################### #################################### Okta OAuth #######################
[auth.okta] [auth.okta]
@ -538,6 +545,7 @@
;allowed_groups = ;allowed_groups =
;role_attribute_path = ;role_attribute_path =
;role_attribute_strict = false ;role_attribute_strict = false
;allow_assign_grafana_admin = false
#################################### Generic OAuth ########################## #################################### Generic OAuth ##########################
[auth.generic_oauth] [auth.generic_oauth]
@ -570,6 +578,7 @@
;tls_client_ca = ;tls_client_ca =
;use_pkce = false ;use_pkce = false
;auth_style = ;auth_style =
;allow_assign_grafana_admin = false
#################################### Basic Auth ########################## #################################### Basic Auth ##########################
[auth.basic] [auth.basic]

View File

@ -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 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 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 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 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 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 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 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 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 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 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 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 0f3d47bb-002a-4cd0-a502-725f224308a7 bdce2246-bb51-4f55-bb81-b7b8856225bc
f1311ecb-6a6a-49d6-bb16-5132daf93a64 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 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
\. \.

View File

@ -26,7 +26,8 @@ name_attribute_path = name
auth_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/auth auth_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/auth
token_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/token token_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/token
api_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/userinfo 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 ## 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: http://localhost:8087
- keycloak admin login: admin:admin - keycloak admin login: admin:admin
- grafana oauth viewer login: oauth-viewer:grafana - grafana oauth viewer login: oauth-viewer:grafana
- grafana oauth editor login: oauth-editor:grafana - grafana oauth editor login: oauth-editor:grafana
- grafana oauth admin login: oauth-admin:grafana - grafana oauth admin login: oauth-admin:grafana
- grafana oauth server admin login: oauth-grafanaadmin:grafana
# Troubleshooting # Troubleshooting

View File

@ -61,8 +61,8 @@ To enable the Azure AD OAuth2, register your application with Azure AD.
"allowedMemberTypes": [ "allowedMemberTypes": [
"User" "User"
], ],
"description": "Grafana admin Users", "description": "Grafana org admin Users",
"displayName": "Grafana Admin", "displayName": "Grafana Org Admin",
"id": "SOME_UNIQUE_ID", "id": "SOME_UNIQUE_ID",
"isEnabled": true, "isEnabled": true,
"lang": null, "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**. 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 ## Enable Azure AD OAuth in Grafana
1. Add the following to the [Grafana configuration file]({{< relref "../../configure-grafana/#config-file-locations" >}}): 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_domains =
allowed_groups = allowed_groups =
role_attribute_strict = false role_attribute_strict = false
allow_assign_grafana_admin = false
``` ```
You can also use these environment variables to configure **client_id** and **client_secret**: You can also use these environment variables to configure **client_id** and **client_secret**:

View File

@ -296,6 +296,27 @@ Config:
role_attribute_path = contains(info.roles[*], 'admin') && 'Admin' || contains(info.roles[*], 'editor') && 'Editor' || 'Viewer' 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 ### Groups mapping
> Available in Grafana Enterprise v8.1 and later versions. > Available in Grafana Enterprise v8.1 and later versions.

View File

@ -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. 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) ### Team Sync (Enterprise only)
> Only available in Grafana Enterprise v6.3+ > Only available in Grafana Enterprise v6.3+

View File

@ -58,6 +58,9 @@ auth_url = https://gitlab.com/oauth/authorize
token_url = https://gitlab.com/oauth/token token_url = https://gitlab.com/oauth/token
api_url = https://gitlab.com/api/v4 api_url = https://gitlab.com/api/v4
allowed_groups = 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 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. in the URL of the group or subgroup.
Here's a complete example with `allow_sign_up` enabled, with access limited to 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 ```ini
[auth.gitlab] [auth.gitlab]
@ -116,6 +119,8 @@ token_url = https://gitlab.com/oauth/token
api_url = https://gitlab.com/api/v4 api_url = https://gitlab.com/api/v4
allowed_groups = example, foo/bar allowed_groups = example, foo/bar
role_attribute_path = is_admin && 'Admin' || 'Viewer' role_attribute_path = is_admin && 'Admin' || 'Viewer'
role_attribute_strict = true
allow_assign_grafana_admin = false
``` ```
### Map roles ### 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: An example Query could look like the following:
```bash ```ini
role_attribute_path = is_admin && 'Admin' || 'Viewer' 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 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'. ensure those members inherit the role 'Editor'.
```bash ```ini
role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer' role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer'
``` ```
Note: If a match is found in other fields, groups will be ignored. 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) ### Team Sync (Enterprise only)
> Only available in Grafana Enterprise v6.4+ > Only available in Grafana Enterprise v6.4+

View File

@ -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" >}}). 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) ### Team Sync (Enterprise only)
Map your Okta groups to teams in Grafana so that your users will automatically be added to Map your Okta groups to teams in Grafana so that your users will automatically be added to

View File

@ -263,14 +263,15 @@ func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *socia
oauthLogger.Debug("Building external user info from OAuth user info") oauthLogger.Debug("Building external user info from OAuth user info")
extUser := &models.ExternalUserInfo{ extUser := &models.ExternalUserInfo{
AuthModule: fmt.Sprintf("oauth_%s", name), AuthModule: fmt.Sprintf("oauth_%s", name),
OAuthToken: token, OAuthToken: token,
AuthId: userInfo.Id, AuthId: userInfo.Id,
Name: userInfo.Name, Name: userInfo.Name,
Login: userInfo.Login, Login: userInfo.Login,
Email: userInfo.Email, Email: userInfo.Email,
OrgRoles: map[int64]org.RoleType{}, OrgRoles: map[int64]org.RoleType{},
Groups: userInfo.Groups, Groups: userInfo.Groups,
IsGrafanaAdmin: userInfo.IsGrafanaAdmin,
} }
if userInfo.Role != "" && !hs.Cfg.OAuthSkipOrgRoleUpdateSync { if userInfo.Role != "" && !hs.Cfg.OAuthSkipOrgRoleUpdateSync {

View File

@ -3,7 +3,6 @@ package social
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -18,9 +17,7 @@ import (
type SocialAzureAD struct { type SocialAzureAD struct {
*SocialBase *SocialBase
allowedGroups []string allowedGroups []string
autoAssignOrgRole string
roleAttributeStrict bool
} }
type azureClaims struct { type azureClaims struct {
@ -53,7 +50,7 @@ func (s *SocialAzureAD) Type() int {
func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
idToken := token.Extra("id_token") idToken := token.Extra("id_token")
if idToken == nil { if idToken == nil {
return nil, fmt.Errorf("no id_token found") return nil, ErrIDTokenNotFound
} }
parsedToken, err := jwt.ParseSigned(idToken.(string)) parsedToken, err := jwt.ParseSigned(idToken.(string))
@ -68,12 +65,12 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas
email := claims.extractEmail() email := claims.extractEmail()
if email == "" { 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 == "" { 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) 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 return nil, errMissingGroupMembership
} }
var isGrafanaAdmin *bool = nil
if s.allowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
}
return &BasicUserInfo{ return &BasicUserInfo{
Id: claims.ID, Id: claims.ID,
Name: claims.Name, Name: claims.Name,
Email: email, Email: email,
Login: email, Login: email,
Role: string(role), Role: string(role),
Groups: groups, IsGrafanaAdmin: isGrafanaAdmin,
Groups: groups,
}, nil }, nil
} }
@ -123,16 +126,18 @@ func (claims *azureClaims) extractEmail() string {
return claims.Email 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 len(claims.Roles) == 0 {
if strictMode { if strictMode {
return org.RoleType("") return org.RoleType(""), false
} }
return org.RoleType(autoAssignRole) return org.RoleType(autoAssignRole), false
} }
roleOrder := []org.RoleType{ roleOrder := []org.RoleType{
RoleGrafanaAdmin,
org.RoleAdmin, org.RoleAdmin,
org.RoleEditor, org.RoleEditor,
org.RoleViewer, org.RoleViewer,
@ -140,15 +145,19 @@ func (claims *azureClaims) extractRole(autoAssignRole string, strictMode bool) o
for _, role := range roleOrder { for _, role := range roleOrder {
if found := hasRole(claims.Roles, role); found { if found := hasRole(claims.Roles, role); found {
return role if role == RoleGrafanaAdmin {
return org.RoleAdmin, true
}
return role, false
} }
} }
if strictMode { if strictMode {
return org.RoleType("") return org.RoleType(""), false
} }
return org.RoleViewer return org.RoleViewer, false
} }
func hasRole(roles []string, role org.RoleType) bool { func hasRole(roles []string, role org.RoleType) bool {
@ -157,6 +166,7 @@ func hasRole(roles []string, role org.RoleType) bool {
return true return true
} }
} }
return false return false
} }

View File

@ -16,12 +16,20 @@ import (
"gopkg.in/square/go-jose.v2/jwt" "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) { func TestSocialAzureAD_UserInfo(t *testing.T) {
type fields struct { type fields struct {
SocialBase *SocialBase SocialBase *SocialBase
allowedGroups []string allowedGroups []string
autoAssignOrgRole string
roleAttributeStrict bool
} }
type args struct { type args struct {
client *http.Client client *http.Client
@ -46,16 +54,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
autoAssignOrgRole: "Viewer", SocialBase: &SocialBase{autoAssignOrgRole: "Viewer"},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Viewer",
Role: "Viewer", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -86,16 +93,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
autoAssignOrgRole: "Viewer", SocialBase: &SocialBase{autoAssignOrgRole: "Viewer"},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Viewer",
Role: "Viewer", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -108,13 +114,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Admin",
Role: "Admin", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -127,13 +132,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Admin",
Role: "Admin", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -146,13 +150,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Viewer",
Role: "Viewer", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -165,16 +168,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
autoAssignOrgRole: "Editor", SocialBase: &SocialBase{autoAssignOrgRole: "Editor"},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Editor",
Role: "Editor", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -187,13 +189,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Editor",
Role: "Editor", Groups: []string{},
Groups: []string{},
}, },
}, },
{ {
@ -206,13 +207,74 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Admin",
Role: "Admin", Groups: []string{},
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", name: "Error if user is a member of allowed_groups",
fields: fields{ fields: fields{
allowedGroups: []string{"foo", "bar"}, allowedGroups: []string{"foo", "bar"},
autoAssignOrgRole: "Viewer", SocialBase: &SocialBase{autoAssignOrgRole: "Viewer"},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@ -246,13 +308,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
Name: "My Name", Name: "My Name",
Email: "me@example.com", Email: "me@example.com",
Login: "me@example.com", Login: "me@example.com",
Company: "", Role: "Viewer",
Role: "Viewer", Groups: []string{"foo"},
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", name: "Fetch empty role when strict attribute role is true and no match",
fields: fields{ fields: fields{
roleAttributeStrict: true, SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, ""),
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", 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", name: "Fetch empty role when strict attribute role is true and no role claims returned",
fields: fields{ fields: fields{
roleAttributeStrict: true, SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, ""),
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@ -313,13 +374,16 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
wantErr: true, wantErr: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &SocialAzureAD{ s := &SocialAzureAD{
SocialBase: tt.fields.SocialBase, SocialBase: tt.fields.SocialBase,
allowedGroups: tt.fields.allowedGroups, allowedGroups: tt.fields.allowedGroups,
autoAssignOrgRole: tt.fields.autoAssignOrgRole, }
roleAttributeStrict: tt.fields.roleAttributeStrict,
if tt.fields.SocialBase == nil {
s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "")
} }
key := []byte("secret") key := []byte("secret")
@ -357,14 +421,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
} }
} }
raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize() raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize()
if err != nil { require.NoError(t, err)
t.Error(err)
}
} else { } else {
raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize() raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize()
if err != nil { require.NoError(t, err)
t.Error(err)
}
} }
token := &oauth2.Token{ token := &oauth2.Token{

View File

@ -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")
)

View File

@ -14,7 +14,6 @@ import (
"strconv" "strconv"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/org"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -27,8 +26,6 @@ type SocialGenericOAuth struct {
emailAttributePath string emailAttributePath string
loginAttributePath string loginAttributePath string
nameAttributePath string nameAttributePath string
roleAttributePath string
roleAttributeStrict bool
groupsAttributePath string groupsAttributePath string
idTokenAttributeName string idTokenAttributeName string
teamIdsAttributePath string teamIdsAttributePath string
@ -147,12 +144,18 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
} }
if userInfo.Role == "" { if userInfo.Role == "" {
role, err := s.extractRole(data) role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, []string{})
if err != nil { if role != "" {
s.log.Warn("Failed to extract role", "error", err) if s.roleAttributeStrict && !role.IsValid() {
} else if role != "" { return nil, ErrInvalidBasicRole
}
s.log.Debug("Setting user info role from extracted role") 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 userInfo.Login = userInfo.Email
} }
if s.roleAttributeStrict && !org.RoleType(userInfo.Role).IsValid() {
return nil, errors.New("invalid role")
}
if !s.IsTeamMember(client) { if !s.IsTeamMember(client) {
return nil, errors.New("user not a member of one of the required teams") 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 "" 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) { func (s *SocialGenericOAuth) extractGroups(data *UserInfoJson) ([]string, error) {
if s.groupsAttributePath == "" { if s.groupsAttributePath == "" {
return []string{}, nil return []string{}, nil

View File

@ -245,12 +245,14 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
} }
tests := []struct { tests := []struct {
Name string Name string
ResponseBody interface{} AllowAssignGrafanaAdmin bool
OAuth2Extra interface{} ResponseBody interface{}
RoleAttributePath string OAuth2Extra interface{}
ExpectedEmail string RoleAttributePath string
ExpectedRole string ExpectedEmail string
ExpectedRole string
ExpectedGrafanaAdmin *bool
}{ }{
{ {
Name: "Given a valid id_token, a valid role path, no API response, use id_token", 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", ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin", 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", Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token",
OAuth2Extra: map[string]interface{}{ OAuth2Extra: map[string]interface{}{
@ -368,7 +402,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
}, },
RoleAttributePath: "role", RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com", ExpectedEmail: "john.doe@example.com",
ExpectedRole: "FromResponse", ExpectedRole: "Fromresponse",
}, },
{ {
Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role", 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 { for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath provider.roleAttributePath = test.RoleAttributePath
provider.allowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
body, err := json.Marshal(test.ResponseBody) body, err := json.Marshal(test.ResponseBody)
require.NoError(t, err) 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.Email)
require.Equal(t, test.ExpectedEmail, actualResult.Login) require.Equal(t, test.ExpectedEmail, actualResult.Login)
require.Equal(t, test.ExpectedRole, actualResult.Role) require.Equal(t, test.ExpectedRole, actualResult.Role)
require.Equal(t, test.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
}) })
} }
}) })

View File

@ -201,21 +201,24 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
teams := convertToGroupList(teamMemberships) teams := convertToGroupList(teamMemberships)
role, err := s.extractRole(response.Body, teams) role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, teams)
if err != nil {
s.log.Error("Failed to extract role", "error", err)
}
if s.roleAttributeStrict && !role.IsValid() { 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{ userInfo := &BasicUserInfo{
Name: data.Login, Name: data.Login,
Login: data.Login, Login: data.Login,
Id: fmt.Sprintf("%d", data.Id), Id: fmt.Sprintf("%d", data.Id),
Email: data.Email, Email: data.Email,
Role: string(role), Role: string(role),
Groups: teams, Groups: teams,
IsGrafanaAdmin: isGrafanaAdmin,
} }
if data.Name != "" { if data.Name != "" {
userInfo.Name = data.Name userInfo.Name = data.Name

View File

@ -127,13 +127,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
autoAssignOrgRole: "", autoAssignOrgRole: "",
roleAttributePath: "", roleAttributePath: "",
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1", Id: "1",
Name: "monalisa octocat", Name: "monalisa octocat",
Email: "octocat@github.com", Email: "octocat@github.com",
Login: "octocat", Login: "octocat",
Company: "", Role: "",
Role: "", Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
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", autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON, userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1", Id: "1",
Name: "monalisa octocat", Name: "monalisa octocat",
Email: "octocat@github.com", Email: "octocat@github.com",
Login: "octocat", Login: "octocat",
Company: "", Role: "Admin",
Role: "Admin", Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
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", autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON, userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1", Id: "1",
Name: "monalisa octocat", Name: "monalisa octocat",
Email: "octocat@github.com", Email: "octocat@github.com",
Login: "octocat", Login: "octocat",
Company: "", Role: "Editor",
Role: "Editor", Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
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", autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON, userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1", Id: "1",
Name: "monalisa octocat", Name: "monalisa octocat",
Email: "octocat@github.com", Email: "octocat@github.com",
Login: "octocat", Login: "octocat",
Company: "", Role: "Editor",
Role: "Editor", Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
}, },
}, },
} }

View File

@ -2,7 +2,6 @@ package social
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@ -114,21 +113,24 @@ func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
groups := s.GetGroups(client) groups := s.GetGroups(client)
role, err := s.extractRole(response.Body, groups) role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, groups)
if err != nil {
s.log.Error("Failed to extract role", "error", err)
}
if s.roleAttributeStrict && !role.IsValid() { 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{ userInfo := &BasicUserInfo{
Id: fmt.Sprintf("%d", data.Id), Id: fmt.Sprintf("%d", data.Id),
Name: data.Name, Name: data.Name,
Login: data.Username, Login: data.Username,
Email: data.Email, Email: data.Email,
Groups: groups, Groups: groups,
Role: string(role), Role: string(role),
IsGrafanaAdmin: isGrafanaAdmin,
} }
if !s.IsGroupMember(groups) { if !s.IsGroupMember(groups) {

View File

@ -7,17 +7,14 @@ import (
"net/http" "net/http"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/org"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2/jwt" "gopkg.in/square/go-jose.v2/jwt"
) )
type SocialOkta struct { type SocialOkta struct {
*SocialBase *SocialBase
apiUrl string apiUrl string
allowedGroups []string allowedGroups []string
roleAttributePath string
roleAttributeStrict bool
} }
type OktaUserInfoJson struct { type OktaUserInfoJson struct {
@ -78,26 +75,29 @@ func (s *SocialOkta) UserInfo(client *http.Client, token *oauth2.Token) (*BasicU
return nil, err 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) groups := s.GetGroups(&data)
if !s.IsGroupMember(groups) { if !s.IsGroupMember(groups) {
return nil, errMissingGroupMembership 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{ return &BasicUserInfo{
Id: claims.ID, Id: claims.ID,
Name: claims.Name, Name: claims.Name,
Email: email, Email: email,
Login: email, Login: email,
Role: role, Role: string(role),
Groups: groups, IsGrafanaAdmin: isGrafanaAdmin,
Groups: groups,
}, nil }, nil
} }
@ -120,18 +120,6 @@ func (s *SocialOkta) extractAPI(data *OktaUserInfoJson, client *http.Client) err
return nil 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 { func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string {
groups := make([]string, 0) groups := make([]string, 0)
if len(data.Groups) > 0 { if len(data.Groups) > 0 {

View File

@ -12,6 +12,8 @@ import (
"context" "context"
"golang.org/x/oauth2" "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/infra/log"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -31,28 +33,29 @@ type SocialService struct {
} }
type OAuthInfo struct { type OAuthInfo struct {
ClientId, ClientSecret string ClientId, ClientSecret string
Scopes []string Scopes []string
AuthUrl, TokenUrl string AuthUrl, TokenUrl string
Enabled bool Enabled bool
EmailAttributeName string EmailAttributeName string
EmailAttributePath string EmailAttributePath string
RoleAttributePath string RoleAttributePath string
RoleAttributeStrict bool RoleAttributeStrict bool
GroupsAttributePath string GroupsAttributePath string
TeamIdsAttributePath string TeamIdsAttributePath string
AllowedDomains []string AllowedDomains []string
HostedDomain string AllowAssignGrafanaAdmin bool
ApiUrl string HostedDomain string
TeamsUrl string ApiUrl string
AllowSignup bool TeamsUrl string
Name string AllowSignup bool
Icon string Name string
TlsClientCert string Icon string
TlsClientKey string TlsClientCert string
TlsClientCa string TlsClientKey string
TlsSkipVerify bool TlsClientCa string
UsePKCE bool TlsSkipVerify bool
UsePKCE bool
} }
func ProvideService(cfg *setting.Cfg) *SocialService { func ProvideService(cfg *setting.Cfg) *SocialService {
@ -66,30 +69,31 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
sec := cfg.Raw.Section("auth." + name) sec := cfg.Raw.Section("auth." + name)
info := &OAuthInfo{ info := &OAuthInfo{
ClientId: sec.Key("client_id").String(), ClientId: sec.Key("client_id").String(),
ClientSecret: sec.Key("client_secret").String(), ClientSecret: sec.Key("client_secret").String(),
Scopes: util.SplitString(sec.Key("scopes").String()), Scopes: util.SplitString(sec.Key("scopes").String()),
AuthUrl: sec.Key("auth_url").String(), AuthUrl: sec.Key("auth_url").String(),
TokenUrl: sec.Key("token_url").String(), TokenUrl: sec.Key("token_url").String(),
ApiUrl: sec.Key("api_url").String(), ApiUrl: sec.Key("api_url").String(),
TeamsUrl: sec.Key("teams_url").String(), TeamsUrl: sec.Key("teams_url").String(),
Enabled: sec.Key("enabled").MustBool(), Enabled: sec.Key("enabled").MustBool(),
EmailAttributeName: sec.Key("email_attribute_name").String(), EmailAttributeName: sec.Key("email_attribute_name").String(),
EmailAttributePath: sec.Key("email_attribute_path").String(), EmailAttributePath: sec.Key("email_attribute_path").String(),
RoleAttributePath: sec.Key("role_attribute_path").String(), RoleAttributePath: sec.Key("role_attribute_path").String(),
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(), RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
GroupsAttributePath: sec.Key("groups_attribute_path").String(), GroupsAttributePath: sec.Key("groups_attribute_path").String(),
TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(), TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(), HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(), AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name), Name: sec.Key("name").MustString(name),
Icon: sec.Key("icon").String(), Icon: sec.Key("icon").String(),
TlsClientCert: sec.Key("tls_client_cert").String(), TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(), TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(), TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
UsePKCE: sec.Key("use_pkce").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 // when empty_scopes parameter exists and is true, overwrite scope with empty value
@ -163,21 +167,17 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// AzureAD. // AzureAD.
if name == "azuread" { if name == "azuread" {
ss.socialMap["azuread"] = &SocialAzureAD{ ss.socialMap["azuread"] = &SocialAzureAD{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole), SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole),
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
autoAssignOrgRole: cfg.AutoAssignOrgRole,
roleAttributeStrict: info.RoleAttributeStrict,
} }
} }
// Okta // Okta
if name == "okta" { if name == "okta" {
ss.socialMap["okta"] = &SocialOkta{ ss.socialMap["okta"] = &SocialOkta{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole), SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole),
apiUrl: info.ApiUrl, apiUrl: info.ApiUrl,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
} }
} }
@ -190,8 +190,6 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
emailAttributeName: info.EmailAttributeName, emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath, emailAttributePath: info.EmailAttributePath,
nameAttributePath: sec.Key("name_attribute_path").String(), nameAttributePath: sec.Key("name_attribute_path").String(),
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
groupsAttributePath: info.GroupsAttributePath, groupsAttributePath: info.GroupsAttributePath,
loginAttributePath: sec.Key("login_attribute_path").String(), loginAttributePath: sec.Key("login_attribute_path").String(),
idTokenAttributeName: sec.Key("id_token_attribute_name").String(), idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
@ -226,18 +224,18 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
} }
type BasicUserInfo struct { type BasicUserInfo struct {
Id string Id string
Name string Name string
Email string Email string
Login string Login string
Company string Role string
Role string IsGrafanaAdmin *bool // nil will avoid overriding user's set server admin setting
Groups []string Groups []string
} }
func (b *BasicUserInfo) String() string { func (b *BasicUserInfo) String() string {
return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Company: %s, Role: %s, Groups: %v", return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Role: %s, Groups: %v",
b.Id, b.Name, b.Email, b.Login, b.Company, b.Role, b.Groups) b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups)
} }
type SocialConnector interface { type SocialConnector interface {
@ -254,9 +252,10 @@ type SocialConnector interface {
type SocialBase struct { type SocialBase struct {
*oauth2.Config *oauth2.Config
log log.Logger log log.Logger
allowSignup bool allowSignup bool
allowedDomains []string allowAssignGrafanaAdmin bool
allowedDomains []string
roleAttributePath string roleAttributePath string
roleAttributeStrict bool roleAttributeStrict bool
@ -272,7 +271,8 @@ func (e Error) Error() string {
} }
const ( const (
grafanaCom = "grafana_com" grafanaCom = "grafana_com"
RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces
) )
var ( var (
@ -297,13 +297,14 @@ func newSocialBase(name string,
logger := log.New("oauth." + name) logger := log.New("oauth." + name)
return &SocialBase{ return &SocialBase{
Config: config, Config: config,
log: logger, log: logger,
allowSignup: info.AllowSignup, allowSignup: info.AllowSignup,
allowedDomains: info.AllowedDomains, allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
autoAssignOrgRole: autoAssignOrgRole, allowedDomains: info.AllowedDomains,
roleAttributePath: info.RoleAttributePath, autoAssignOrgRole: autoAssignOrgRole,
roleAttributeStrict: info.RoleAttributeStrict, roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
} }
} }
@ -311,28 +312,38 @@ type groupStruct struct {
Groups []string `json:"groups"` 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.roleAttributePath == "" {
if s.autoAssignOrgRole != "" { 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) role, err := s.searchJSONForStringAttr(s.roleAttributePath, rawJSON)
if err == nil && role != "" { if err == nil && role != "" {
return org.RoleType(role), nil return getRoleFromSearch(role)
} }
if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil { if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil {
if role, err := s.searchJSONForStringAttr( role, err := s.searchJSONForStringAttr(s.roleAttributePath, groupBytes)
s.roleAttributePath, groupBytes); err == nil && role != "" { if err == nil && role != "" {
return org.RoleType(role), nil 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 // GetOAuthProviders returns available oauth providers and if they're enabled or not