mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
1353177e15
commit
ef245874da
@ -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]
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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**:
|
||||||
|
@ -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.
|
||||||
|
@ -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+
|
||||||
|
@ -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+
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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{
|
||||||
|
9
pkg/login/social/errors.go
Normal file
9
pkg/login/social/errors.go
Normal 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")
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user