mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 06:32:21 +08:00
Auth: Add support for role mapping and allowed groups in Google OIDC (#76266)
* support google oauth allowed_groups. unify allowed groups logic * add role mapping for google oauth * add documentation * add addendums * remove extra isGroupMember * add to sample ini * Apply suggestions from code review Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
@ -644,7 +644,11 @@ token_url = https://oauth2.googleapis.com/token
|
|||||||
api_url = https://openidconnect.googleapis.com/v1/userinfo
|
api_url = https://openidconnect.googleapis.com/v1/userinfo
|
||||||
allowed_domains =
|
allowed_domains =
|
||||||
hosted_domain =
|
hosted_domain =
|
||||||
skip_org_role_sync = false
|
allowed_groups =
|
||||||
|
role_attribute_path =
|
||||||
|
role_attribute_strict = false
|
||||||
|
allow_assign_grafana_admin = false
|
||||||
|
skip_org_role_sync = true
|
||||||
tls_skip_verify_insecure = false
|
tls_skip_verify_insecure = false
|
||||||
tls_client_cert =
|
tls_client_cert =
|
||||||
tls_client_key =
|
tls_client_key =
|
||||||
|
@ -626,6 +626,10 @@
|
|||||||
;api_url = https://openidconnect.googleapis.com/v1/userinfo
|
;api_url = https://openidconnect.googleapis.com/v1/userinfo
|
||||||
;allowed_domains =
|
;allowed_domains =
|
||||||
;hosted_domain =
|
;hosted_domain =
|
||||||
|
;allowed_groups =
|
||||||
|
;role_attribute_path =
|
||||||
|
;role_attribute_strict = false
|
||||||
|
;allow_assign_grafana_admin = false
|
||||||
;skip_org_role_sync = false
|
;skip_org_role_sync = false
|
||||||
;use_pkce = true
|
;use_pkce = true
|
||||||
|
|
||||||
|
@ -79,15 +79,15 @@ The table below describes all GitLab OAuth configuration options. Like any other
|
|||||||
| `api_url` | No | Grafana uses `<api_url>/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` |
|
| `api_url` | No | Grafana uses `<api_url>/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` |
|
||||||
| `name` | No | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` |
|
| `name` | No | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` |
|
||||||
| `icon` | No | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` |
|
| `icon` | No | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` |
|
||||||
| `scopes` | No | List of comma- or space-separated GitLab OAuth scopes. | `openid email profile` |
|
| `scopes` | No | List of comma or space-separated GitLab OAuth scopes. | `openid email profile` |
|
||||||
| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` |
|
| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` |
|
||||||
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
|
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
|
||||||
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
|
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
|
||||||
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
|
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
|
||||||
| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
|
| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
|
||||||
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` |
|
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` |
|
||||||
| `allowed_domains` | No | List of comma- or space-separated domains. User must belong to at least one domain to log in. | |
|
| `allowed_domains` | No | List of comma or space-separated domains. User must belong to at least one domain to log in. | |
|
||||||
| `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. | |
|
| `allowed_groups` | No | List of comma or space-separated groups. The user should be a member of at least one group to log in. | |
|
||||||
| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
|
| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
|
||||||
| `tls_client_cert` | No | The path to the certificate. | |
|
| `tls_client_cert` | No | The path to the certificate. | |
|
||||||
| `tls_client_key` | No | The path to the key. | |
|
| `tls_client_key` | No | The path to the key. | |
|
||||||
@ -115,7 +115,7 @@ Refresh token fetching and access token expiration check is enabled by default f
|
|||||||
|
|
||||||
To limit access to authenticated users that are members of one or more [GitLab
|
To limit access to authenticated users that are members of one or more [GitLab
|
||||||
groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
|
groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
|
||||||
to a comma- or space-separated list of groups.
|
to a comma or space-separated list of groups.
|
||||||
|
|
||||||
GitLab's groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`.
|
GitLab's groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`.
|
||||||
Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters.
|
Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters.
|
||||||
|
@ -100,16 +100,6 @@ This setting is ignored if multiple auth providers are configured to use auto lo
|
|||||||
auto_login = true
|
auto_login = true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skip organization role sync
|
|
||||||
|
|
||||||
We do not currently sync roles from Google and instead set the AutoAssigned role to the user at first login. The default setting for `skip_org_role_sync` is `true`, which means that role modifications can still be made through the user interface.
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[auth.google]
|
|
||||||
# ..
|
|
||||||
skip_org_role_sync = true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configure team sync for Google OAuth
|
### Configure team sync for Google OAuth
|
||||||
|
|
||||||
> Available in Grafana v10.1.0 and later versions.
|
> Available in Grafana v10.1.0 and later versions.
|
||||||
@ -132,3 +122,68 @@ With team sync, you can easily add users to teams by utilizing their Google grou
|
|||||||
The external group ID for a Google group is the group's email address, such as `dev@grafana.com`.
|
The external group ID for a Google group is the group's email address, such as `dev@grafana.com`.
|
||||||
|
|
||||||
To learn more about Team Sync, refer to [Configure Team Sync]({{< relref "../../configure-team-sync" >}}).
|
To learn more about Team Sync, refer to [Configure Team Sync]({{< relref "../../configure-team-sync" >}}).
|
||||||
|
|
||||||
|
### Configure allowed groups
|
||||||
|
|
||||||
|
> Available in Grafana v10.2.0 and later versions.
|
||||||
|
|
||||||
|
To limit access to authenticated users that are members of one or more groups, set `allowed_groups`
|
||||||
|
to a comma or space separated list of groups.
|
||||||
|
|
||||||
|
Google groups are referenced by the group email key. For example, `developers@google.com`.
|
||||||
|
|
||||||
|
> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups
|
||||||
|
|
||||||
|
## Configure role mapping
|
||||||
|
|
||||||
|
> Available in Grafana v10.2.0 and later versions.
|
||||||
|
|
||||||
|
Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role mapped from Google upon user login. If no mapping is set the default instance role is used.
|
||||||
|
|
||||||
|
The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option.
|
||||||
|
To map the server administrator role, use the `allow_assign_grafana_admin` configuration option.
|
||||||
|
|
||||||
|
If no valid role is found, the user is assigned the role specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}).
|
||||||
|
You can disable this default role assignment by setting `role_attribute_strict = true`.
|
||||||
|
This setting denies user access if no role or an invalid role is returned.
|
||||||
|
|
||||||
|
To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads.
|
||||||
|
|
||||||
|
> By default skip_org_role_sync is enabled. skip_org_role_sync will default to false in Grafana v10.3.0 and later versions.
|
||||||
|
|
||||||
|
### Role mapping examples
|
||||||
|
|
||||||
|
This section includes examples of JMESPath expressions used for role mapping.
|
||||||
|
|
||||||
|
#### Map roles using user information from OAuth token
|
||||||
|
|
||||||
|
In this example, the user with email `admin@company.com` has been granted the `Admin` role.
|
||||||
|
All other users are granted the `Viewer` role.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
role_attribute_path = email=='admin@company.com' && 'Admin' || 'Viewer'
|
||||||
|
skip_org_role_sync = false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Map roles using groups
|
||||||
|
|
||||||
|
In this example, the user from Google group 'example-group@google.com' have been granted the `Editor` role.
|
||||||
|
All other users are granted the `Viewer` role.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
role_attribute_path = contains(groups[*], 'example-group@google.com') && 'Editor' || 'Viewer'
|
||||||
|
skip_org_role_sync = false
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups
|
||||||
|
|
||||||
|
#### Map server administrator role
|
||||||
|
|
||||||
|
In this example, the user with email `admin@company.com` has been granted the `Admin` organization role as well as the Grafana server admin role.
|
||||||
|
All other users are granted the `Viewer` role.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
allow_assign_grafana_admin = true
|
||||||
|
skip_org_role_sync = false
|
||||||
|
role_attribute_path = email=='admin@company.com' && 'GrafanaAdmin' || 'Viewer'
|
||||||
|
```
|
||||||
|
@ -23,7 +23,6 @@ type SocialAzureAD struct {
|
|||||||
*SocialBase
|
*SocialBase
|
||||||
cache remotecache.CacheStorage
|
cache remotecache.CacheStorage
|
||||||
allowedOrganizations []string
|
allowedOrganizations []string
|
||||||
allowedGroups []string
|
|
||||||
forceUseGraphAPI bool
|
forceUseGraphAPI bool
|
||||||
skipOrgRoleSync bool
|
skipOrgRoleSync bool
|
||||||
}
|
}
|
||||||
@ -99,7 +98,7 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
|
|||||||
return nil, fmt.Errorf("failed to extract groups: %w", err)
|
return nil, fmt.Errorf("failed to extract groups: %w", err)
|
||||||
}
|
}
|
||||||
s.log.Debug("AzureAD OAuth: extracted groups", "email", email, "groups", fmt.Sprintf("%v", groups))
|
s.log.Debug("AzureAD OAuth: extracted groups", "email", email, "groups", fmt.Sprintf("%v", groups))
|
||||||
if !s.IsGroupMember(groups) {
|
if !s.isGroupMember(groups) {
|
||||||
return nil, errMissingGroupMembership
|
return nil, errMissingGroupMembership
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,22 +181,6 @@ func (s *SocialAzureAD) validateIDTokenSignature(ctx context.Context, client *ht
|
|||||||
return nil, &Error{"AzureAD OAuth: signing key not found"}
|
return nil, &Error{"AzureAD OAuth: signing key not found"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialAzureAD) IsGroupMember(groups []string) bool {
|
|
||||||
if len(s.allowedGroups) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, allowedGroup := range s.allowedGroups {
|
|
||||||
for _, group := range groups {
|
|
||||||
if group == allowedGroup {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (claims *azureClaims) extractEmail() string {
|
func (claims *azureClaims) extractEmail() string {
|
||||||
if claims.Email == "" {
|
if claims.Email == "" {
|
||||||
if claims.PreferredUsername != "" {
|
if claims.PreferredUsername != "" {
|
||||||
|
@ -530,7 +530,6 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
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,
|
|
||||||
allowedOrganizations: tt.fields.allowedOrganizations,
|
allowedOrganizations: tt.fields.allowedOrganizations,
|
||||||
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
@ -540,6 +539,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
|
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tt.fields.allowedGroups != nil {
|
||||||
|
s.allowedGroups = tt.fields.allowedGroups
|
||||||
|
}
|
||||||
|
|
||||||
if tt.fields.usGovURL {
|
if tt.fields.usGovURL {
|
||||||
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
||||||
} else {
|
} else {
|
||||||
@ -710,14 +713,15 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
|||||||
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,
|
|
||||||
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
||||||
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.fields.SocialBase == nil {
|
if tt.fields.SocialBase == nil {
|
||||||
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
|
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{
|
||||||
|
AllowedGroups: tt.fields.allowedGroups,
|
||||||
|
}, "", false, *featuremgmt.WithFeatures())
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SocialBase.Endpoint.AuthURL = authURL
|
s.SocialBase.Endpoint.AuthURL = authURL
|
||||||
|
@ -22,7 +22,6 @@ const (
|
|||||||
|
|
||||||
type SocialGitlab struct {
|
type SocialGitlab struct {
|
||||||
*SocialBase
|
*SocialBase
|
||||||
allowedGroups []string
|
|
||||||
apiUrl string
|
apiUrl string
|
||||||
skipOrgRoleSync bool
|
skipOrgRoleSync bool
|
||||||
}
|
}
|
||||||
@ -48,22 +47,6 @@ type userData struct {
|
|||||||
IsGrafanaAdmin *bool `json:"-"`
|
IsGrafanaAdmin *bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGitlab) isGroupMember(groups []string) bool {
|
|
||||||
if len(s.allowedGroups) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, allowedGroup := range s.allowedGroups {
|
|
||||||
for _, group := range groups {
|
|
||||||
if group == allowedGroup {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
|
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
|
||||||
groups := make([]string, 0)
|
groups := make([]string, 0)
|
||||||
nextPage := new(int)
|
nextPage := new(int)
|
||||||
|
@ -30,6 +30,7 @@ type googleUserData struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
EmailVerified bool `json:"email_verified"`
|
EmailVerified bool `json:"email_verified"`
|
||||||
|
rawJSON []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
@ -59,6 +60,10 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
|
|||||||
s.log.Warn("Error retrieving groups", "error", errPage)
|
s.log.Warn("Error retrieving groups", "error", errPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !s.isGroupMember(groups) {
|
||||||
|
return nil, errMissingGroupMembership
|
||||||
|
}
|
||||||
|
|
||||||
userInfo := &BasicUserInfo{
|
userInfo := &BasicUserInfo{
|
||||||
Id: data.ID,
|
Id: data.ID,
|
||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
@ -69,6 +74,19 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
|
|||||||
Groups: groups,
|
Groups: groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !s.skipOrgRoleSync {
|
||||||
|
role, grafanaAdmin, errRole := s.extractRoleAndAdmin(data.rawJSON, groups)
|
||||||
|
if errRole != nil {
|
||||||
|
return nil, errRole
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.allowAssignGrafanaAdmin {
|
||||||
|
userInfo.IsGrafanaAdmin = &grafanaAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo.Role = role
|
||||||
|
}
|
||||||
|
|
||||||
s.log.Debug("Resolved user info", "data", fmt.Sprintf("%+v", userInfo))
|
s.log.Debug("Resolved user info", "data", fmt.Sprintf("%+v", userInfo))
|
||||||
|
|
||||||
return userInfo, nil
|
return userInfo, nil
|
||||||
@ -98,6 +116,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
|
|||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
Email: data.Email,
|
Email: data.Email,
|
||||||
EmailVerified: data.EmailVerified,
|
EmailVerified: data.EmailVerified,
|
||||||
|
rawJSON: response.Body,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +164,8 @@ func (s *SocialGoogle) extractFromToken(ctx context.Context, client *http.Client
|
|||||||
return nil, fmt.Errorf("Error getting user info: %s", err)
|
return nil, fmt.Errorf("Error getting user info: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.rawJSON = rawJSON
|
||||||
|
|
||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSocialGoogle_retrieveGroups(t *testing.T) {
|
func TestSocialGoogle_retrieveGroups(t *testing.T) {
|
||||||
@ -241,6 +242,11 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
type fields struct {
|
type fields struct {
|
||||||
Scopes []string
|
Scopes []string
|
||||||
apiURL string
|
apiURL string
|
||||||
|
allowedGroups []string
|
||||||
|
roleAttributePath string
|
||||||
|
roleAttributeStrict bool
|
||||||
|
allowAssignGrafanaAdmin bool
|
||||||
|
skipOrgRoleSync bool
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
@ -258,6 +264,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
name: "Success id_token",
|
name: "Success id_token",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
|
skipOrgRoleSync: true,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
token: tokenWithID,
|
token: tokenWithID,
|
||||||
@ -274,6 +281,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
name: "Success id_token - groups requested",
|
name: "Success id_token - groups requested",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||||
|
skipOrgRoleSync: true,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
token: tokenWithID,
|
token: tokenWithID,
|
||||||
@ -311,6 +319,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
name: "Legacy API URL",
|
name: "Legacy API URL",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
apiURL: legacyAPIURL,
|
apiURL: legacyAPIURL,
|
||||||
|
skipOrgRoleSync: true,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
token: tokenWithoutID,
|
token: tokenWithoutID,
|
||||||
@ -341,6 +350,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
name: "Legacy API URL - no id provided",
|
name: "Legacy API URL - no id provided",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
apiURL: legacyAPIURL,
|
apiURL: legacyAPIURL,
|
||||||
|
skipOrgRoleSync: true,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
token: tokenWithoutID,
|
token: tokenWithoutID,
|
||||||
@ -427,6 +437,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
name: "Success",
|
name: "Success",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
apiURL: "https://openidconnect.googleapis.com/v1/userinfo",
|
apiURL: "https://openidconnect.googleapis.com/v1/userinfo",
|
||||||
|
skipOrgRoleSync: true,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
token: tokenWithoutID,
|
token: tokenWithoutID,
|
||||||
@ -478,6 +489,145 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantErrMsg: "email is not verified",
|
wantErrMsg: "email is not verified",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "not in allowed Groups",
|
||||||
|
fields: fields{
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||||
|
allowedGroups: []string{"not-that-one"},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
token: tokenWithID,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &roundTripperFunc{
|
||||||
|
fn: func(req *http.Request) (*http.Response, error) {
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
_, _ = resp.WriteString(`{
|
||||||
|
"memberships": [
|
||||||
|
{
|
||||||
|
"group": "test-group",
|
||||||
|
"groupKey": {
|
||||||
|
"id": "test-group@google.com"
|
||||||
|
},
|
||||||
|
"displayName": "Test Group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nextPageToken": ""
|
||||||
|
}`)
|
||||||
|
return resp.Result(), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantData: &BasicUserInfo{
|
||||||
|
Id: "88888888888888",
|
||||||
|
Login: "test@example.com",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "Test User",
|
||||||
|
Groups: []string{"test-group@google.com"},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "user not a member of one of the required groups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Role mapping - strict",
|
||||||
|
fields: fields{
|
||||||
|
Scopes: []string{},
|
||||||
|
allowedGroups: []string{},
|
||||||
|
roleAttributePath: "this",
|
||||||
|
roleAttributeStrict: true,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
token: tokenWithID,
|
||||||
|
},
|
||||||
|
wantData: &BasicUserInfo{
|
||||||
|
Id: "88888888888888",
|
||||||
|
Login: "test@example.com",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "Test User",
|
||||||
|
Groups: []string{"test-group@google.com"},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "idP did not return a role attribute, but role_attribute_strict is set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role mapping from id_token - no allowed assign Grafana Admin",
|
||||||
|
fields: fields{
|
||||||
|
Scopes: []string{},
|
||||||
|
allowAssignGrafanaAdmin: false,
|
||||||
|
roleAttributePath: "email_verified && 'GrafanaAdmin'",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
token: tokenWithID,
|
||||||
|
},
|
||||||
|
wantData: &BasicUserInfo{
|
||||||
|
Id: "88888888888888",
|
||||||
|
Login: "test@example.com",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "Test User",
|
||||||
|
Role: roletype.RoleAdmin,
|
||||||
|
IsGrafanaAdmin: nil,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role mapping from id_token - allowed assign Grafana Admin",
|
||||||
|
fields: fields{
|
||||||
|
Scopes: []string{},
|
||||||
|
allowAssignGrafanaAdmin: true,
|
||||||
|
roleAttributePath: "email_verified && 'GrafanaAdmin'",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
token: tokenWithID,
|
||||||
|
},
|
||||||
|
wantData: &BasicUserInfo{
|
||||||
|
Id: "88888888888888",
|
||||||
|
Login: "test@example.com",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "Test User",
|
||||||
|
Role: roletype.RoleAdmin,
|
||||||
|
IsGrafanaAdmin: trueBoolPtr(),
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mapping from groups",
|
||||||
|
fields: fields{
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||||
|
roleAttributePath: "contains(groups[*], 'test-group@google.com') && 'Editor'",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
token: tokenWithID,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &roundTripperFunc{
|
||||||
|
fn: func(req *http.Request) (*http.Response, error) {
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
_, _ = resp.WriteString(`{
|
||||||
|
"memberships": [
|
||||||
|
{
|
||||||
|
"group": "test-group",
|
||||||
|
"groupKey": {
|
||||||
|
"id": "test-group@google.com"
|
||||||
|
},
|
||||||
|
"displayName": "Test Group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nextPageToken": ""
|
||||||
|
}`)
|
||||||
|
return resp.Result(), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantData: &BasicUserInfo{
|
||||||
|
Id: "88888888888888",
|
||||||
|
Login: "test@example.com",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Name: "Test User",
|
||||||
|
Role: "Editor",
|
||||||
|
Groups: []string{"test-group@google.com"},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -488,7 +638,12 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
|||||||
Config: &oauth2.Config{Scopes: tt.fields.Scopes},
|
Config: &oauth2.Config{Scopes: tt.fields.Scopes},
|
||||||
log: log.NewNopLogger(),
|
log: log.NewNopLogger(),
|
||||||
allowSignup: false,
|
allowSignup: false,
|
||||||
|
allowedGroups: tt.fields.allowedGroups,
|
||||||
|
roleAttributePath: tt.fields.roleAttributePath,
|
||||||
|
roleAttributeStrict: tt.fields.roleAttributeStrict,
|
||||||
|
allowAssignGrafanaAdmin: tt.fields.allowAssignGrafanaAdmin,
|
||||||
},
|
},
|
||||||
|
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||||
}
|
}
|
||||||
|
|
||||||
gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token)
|
gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token)
|
||||||
|
@ -63,6 +63,7 @@ type OAuthInfo struct {
|
|||||||
TlsClientKey string `toml:"tls_client_key"`
|
TlsClientKey string `toml:"tls_client_key"`
|
||||||
TokenUrl string `toml:"token_url"`
|
TokenUrl string `toml:"token_url"`
|
||||||
AllowedDomains []string `toml:"allowed_domains"`
|
AllowedDomains []string `toml:"allowed_domains"`
|
||||||
|
AllowedGroups []string `toml:"allowed_groups"`
|
||||||
Scopes []string `toml:"scopes"`
|
Scopes []string `toml:"scopes"`
|
||||||
AllowAssignGrafanaAdmin bool `toml:"allow_assign_grafana_admin"`
|
AllowAssignGrafanaAdmin bool `toml:"allow_assign_grafana_admin"`
|
||||||
AllowSignup bool `toml:"allow_signup"`
|
AllowSignup bool `toml:"allow_signup"`
|
||||||
@ -120,6 +121,7 @@ func ProvideService(cfg *setting.Cfg,
|
|||||||
UseRefreshToken: sec.Key("use_refresh_token").MustBool(false),
|
UseRefreshToken: sec.Key("use_refresh_token").MustBool(false),
|
||||||
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false),
|
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false),
|
||||||
AutoLogin: sec.Key("auto_login").MustBool(false),
|
AutoLogin: sec.Key("auto_login").MustBool(false),
|
||||||
|
AllowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -178,7 +180,6 @@ func ProvideService(cfg *setting.Cfg,
|
|||||||
ss.socialMap["gitlab"] = &SocialGitlab{
|
ss.socialMap["gitlab"] = &SocialGitlab{
|
||||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||||
apiUrl: info.ApiUrl,
|
apiUrl: info.ApiUrl,
|
||||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
|
||||||
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
|
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,7 +203,6 @@ func ProvideService(cfg *setting.Cfg,
|
|||||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
|
||||||
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
|
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
|
||||||
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
|
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
|
||||||
}
|
}
|
||||||
@ -305,6 +305,7 @@ type SocialBase struct {
|
|||||||
allowSignup bool
|
allowSignup bool
|
||||||
allowAssignGrafanaAdmin bool
|
allowAssignGrafanaAdmin bool
|
||||||
allowedDomains []string
|
allowedDomains []string
|
||||||
|
allowedGroups []string
|
||||||
|
|
||||||
roleAttributePath string
|
roleAttributePath string
|
||||||
roleAttributeStrict bool
|
roleAttributeStrict bool
|
||||||
@ -356,9 +357,10 @@ func newSocialBase(name string,
|
|||||||
allowSignup: info.AllowSignup,
|
allowSignup: info.AllowSignup,
|
||||||
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
|
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
|
||||||
allowedDomains: info.AllowedDomains,
|
allowedDomains: info.AllowedDomains,
|
||||||
autoAssignOrgRole: autoAssignOrgRole,
|
allowedGroups: info.AllowedGroups,
|
||||||
roleAttributePath: info.RoleAttributePath,
|
roleAttributePath: info.RoleAttributePath,
|
||||||
roleAttributeStrict: info.RoleAttributeStrict,
|
roleAttributeStrict: info.RoleAttributeStrict,
|
||||||
|
autoAssignOrgRole: autoAssignOrgRole,
|
||||||
skipOrgRoleSync: skipOrgRoleSync,
|
skipOrgRoleSync: skipOrgRoleSync,
|
||||||
features: features,
|
features: features,
|
||||||
useRefreshToken: info.UseRefreshToken,
|
useRefreshToken: info.UseRefreshToken,
|
||||||
@ -571,6 +573,22 @@ func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]interfac
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SocialBase) isGroupMember(groups []string) bool {
|
||||||
|
if len(s.allowedGroups) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowedGroup := range s.allowedGroups {
|
||||||
|
for _, group := range groups {
|
||||||
|
if group == allowedGroup {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
|
func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
|
||||||
tokenString, ok := idToken.(string)
|
tokenString, ok := idToken.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -1497,9 +1497,7 @@ func readAuthGithubSettings(cfg *Cfg) {
|
|||||||
func readAuthGoogleSettings(cfg *Cfg) {
|
func readAuthGoogleSettings(cfg *Cfg) {
|
||||||
sec := cfg.SectionWithEnvOverrides("auth.google")
|
sec := cfg.SectionWithEnvOverrides("auth.google")
|
||||||
cfg.GoogleAuthEnabled = sec.Key("enabled").MustBool(false)
|
cfg.GoogleAuthEnabled = sec.Key("enabled").MustBool(false)
|
||||||
// FIXME: for now we skip org role sync for google auth
|
cfg.GoogleSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(true)
|
||||||
// as we do not sync organization roles from Google
|
|
||||||
cfg.GoogleSkipOrgRoleSync = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAuthGitlabSettings(cfg *Cfg) {
|
func readAuthGitlabSettings(cfg *Cfg) {
|
||||||
|
Reference in New Issue
Block a user