mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06: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:
parent
0d390382d3
commit
cada1f040a
@ -644,7 +644,11 @@ token_url = https://oauth2.googleapis.com/token
|
||||
api_url = https://openidconnect.googleapis.com/v1/userinfo
|
||||
allowed_domains =
|
||||
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_client_cert =
|
||||
tls_client_key =
|
||||
@ -828,7 +832,7 @@ assume_role_enabled = true
|
||||
list_metrics_page_limit = 500
|
||||
|
||||
# Experimental, for use in Grafana Cloud only. Please do not set.
|
||||
external_id =
|
||||
external_id =
|
||||
|
||||
#################################### Azure ###############################
|
||||
[azure]
|
||||
@ -1226,7 +1230,7 @@ url =
|
||||
|
||||
# Tenant ID to use in requests to the Alertmanager.
|
||||
# It will also be used for the basic auth username.
|
||||
tenant =
|
||||
tenant =
|
||||
|
||||
# Optional password for basic authentication.
|
||||
# If not present, the tenant ID will be set in the X-Scope-OrgID header.
|
||||
|
@ -626,6 +626,10 @@
|
||||
;api_url = https://openidconnect.googleapis.com/v1/userinfo
|
||||
;allowed_domains =
|
||||
;hosted_domain =
|
||||
;allowed_groups =
|
||||
;role_attribute_path =
|
||||
;role_attribute_strict = false
|
||||
;allow_assign_grafana_admin = false
|
||||
;skip_org_role_sync = false
|
||||
;use_pkce = true
|
||||
|
||||
@ -781,7 +785,7 @@
|
||||
; list_metrics_page_limit = 500
|
||||
|
||||
# Experimental, for use in Grafana Cloud only. Please do not set.
|
||||
; external_id =
|
||||
; external_id =
|
||||
|
||||
#################################### Azure ###############################
|
||||
[azure]
|
||||
|
@ -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` |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `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_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` |
|
||||
| `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_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group 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. | |
|
||||
| `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_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
|
||||
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`.
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
> 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`.
|
||||
|
||||
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
|
||||
cache remotecache.CacheStorage
|
||||
allowedOrganizations []string
|
||||
allowedGroups []string
|
||||
forceUseGraphAPI 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -182,22 +181,6 @@ func (s *SocialAzureAD) validateIDTokenSignature(ctx context.Context, client *ht
|
||||
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 {
|
||||
if claims.Email == "" {
|
||||
if claims.PreferredUsername != "" {
|
||||
|
@ -530,7 +530,6 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SocialAzureAD{
|
||||
SocialBase: tt.fields.SocialBase,
|
||||
allowedGroups: tt.fields.allowedGroups,
|
||||
allowedOrganizations: tt.fields.allowedOrganizations,
|
||||
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
||||
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())
|
||||
}
|
||||
|
||||
if tt.fields.allowedGroups != nil {
|
||||
s.allowedGroups = tt.fields.allowedGroups
|
||||
}
|
||||
|
||||
if tt.fields.usGovURL {
|
||||
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
||||
} else {
|
||||
@ -710,14 +713,15 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SocialAzureAD{
|
||||
SocialBase: tt.fields.SocialBase,
|
||||
allowedGroups: tt.fields.allowedGroups,
|
||||
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
||||
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -22,7 +22,6 @@ const (
|
||||
|
||||
type SocialGitlab struct {
|
||||
*SocialBase
|
||||
allowedGroups []string
|
||||
apiUrl string
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
@ -48,22 +47,6 @@ type userData struct {
|
||||
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 {
|
||||
groups := make([]string, 0)
|
||||
nextPage := new(int)
|
||||
|
@ -30,6 +30,7 @@ type googleUserData struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
rawJSON []byte `json:"-"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if !s.isGroupMember(groups) {
|
||||
return nil, errMissingGroupMembership
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Id: data.ID,
|
||||
Name: data.Name,
|
||||
@ -69,6 +74,19 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
|
||||
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))
|
||||
|
||||
return userInfo, nil
|
||||
@ -98,6 +116,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
EmailVerified: data.EmailVerified,
|
||||
rawJSON: response.Body,
|
||||
}, 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)
|
||||
}
|
||||
|
||||
data.rawJSON = rawJSON
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
)
|
||||
|
||||
func TestSocialGoogle_retrieveGroups(t *testing.T) {
|
||||
@ -239,8 +240,13 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
tokenWithoutID := &oauth2.Token{}
|
||||
|
||||
type fields struct {
|
||||
Scopes []string
|
||||
apiURL string
|
||||
Scopes []string
|
||||
apiURL string
|
||||
allowedGroups []string
|
||||
roleAttributePath string
|
||||
roleAttributeStrict bool
|
||||
allowAssignGrafanaAdmin bool
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
type args struct {
|
||||
client *http.Client
|
||||
@ -257,7 +263,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Success id_token",
|
||||
fields: fields{
|
||||
Scopes: []string{},
|
||||
Scopes: []string{},
|
||||
skipOrgRoleSync: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
@ -273,7 +280,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Success id_token - groups requested",
|
||||
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{
|
||||
token: tokenWithID,
|
||||
@ -310,7 +318,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Legacy API URL",
|
||||
fields: fields{
|
||||
apiURL: legacyAPIURL,
|
||||
apiURL: legacyAPIURL,
|
||||
skipOrgRoleSync: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithoutID,
|
||||
@ -340,7 +349,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Legacy API URL - no id provided",
|
||||
fields: fields{
|
||||
apiURL: legacyAPIURL,
|
||||
apiURL: legacyAPIURL,
|
||||
skipOrgRoleSync: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithoutID,
|
||||
@ -426,7 +436,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Success",
|
||||
fields: fields{
|
||||
apiURL: "https://openidconnect.googleapis.com/v1/userinfo",
|
||||
apiURL: "https://openidconnect.googleapis.com/v1/userinfo",
|
||||
skipOrgRoleSync: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithoutID,
|
||||
@ -478,6 +489,145 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
wantErr: true,
|
||||
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 {
|
||||
@ -485,10 +635,15 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
s := &SocialGoogle{
|
||||
apiUrl: tt.fields.apiURL,
|
||||
SocialBase: &SocialBase{
|
||||
Config: &oauth2.Config{Scopes: tt.fields.Scopes},
|
||||
log: log.NewNopLogger(),
|
||||
allowSignup: false,
|
||||
Config: &oauth2.Config{Scopes: tt.fields.Scopes},
|
||||
log: log.NewNopLogger(),
|
||||
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)
|
||||
|
@ -63,6 +63,7 @@ type OAuthInfo struct {
|
||||
TlsClientKey string `toml:"tls_client_key"`
|
||||
TokenUrl string `toml:"token_url"`
|
||||
AllowedDomains []string `toml:"allowed_domains"`
|
||||
AllowedGroups []string `toml:"allowed_groups"`
|
||||
Scopes []string `toml:"scopes"`
|
||||
AllowAssignGrafanaAdmin bool `toml:"allow_assign_grafana_admin"`
|
||||
AllowSignup bool `toml:"allow_signup"`
|
||||
@ -120,6 +121,7 @@ func ProvideService(cfg *setting.Cfg,
|
||||
UseRefreshToken: sec.Key("use_refresh_token").MustBool(false),
|
||||
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").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
|
||||
@ -178,7 +180,6 @@ func ProvideService(cfg *setting.Cfg,
|
||||
ss.socialMap["gitlab"] = &SocialGitlab{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
@ -202,7 +203,6 @@ func ProvideService(cfg *setting.Cfg,
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
cache: cache,
|
||||
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),
|
||||
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
|
||||
}
|
||||
@ -305,6 +305,7 @@ type SocialBase struct {
|
||||
allowSignup bool
|
||||
allowAssignGrafanaAdmin bool
|
||||
allowedDomains []string
|
||||
allowedGroups []string
|
||||
|
||||
roleAttributePath string
|
||||
roleAttributeStrict bool
|
||||
@ -356,9 +357,10 @@ func newSocialBase(name string,
|
||||
allowSignup: info.AllowSignup,
|
||||
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
|
||||
allowedDomains: info.AllowedDomains,
|
||||
autoAssignOrgRole: autoAssignOrgRole,
|
||||
allowedGroups: info.AllowedGroups,
|
||||
roleAttributePath: info.RoleAttributePath,
|
||||
roleAttributeStrict: info.RoleAttributeStrict,
|
||||
autoAssignOrgRole: autoAssignOrgRole,
|
||||
skipOrgRoleSync: skipOrgRoleSync,
|
||||
features: features,
|
||||
useRefreshToken: info.UseRefreshToken,
|
||||
@ -571,6 +573,22 @@ func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]interfac
|
||||
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) {
|
||||
tokenString, ok := idToken.(string)
|
||||
if !ok {
|
||||
|
@ -1497,9 +1497,7 @@ func readAuthGithubSettings(cfg *Cfg) {
|
||||
func readAuthGoogleSettings(cfg *Cfg) {
|
||||
sec := cfg.SectionWithEnvOverrides("auth.google")
|
||||
cfg.GoogleAuthEnabled = sec.Key("enabled").MustBool(false)
|
||||
// FIXME: for now we skip org role sync for google auth
|
||||
// as we do not sync organization roles from Google
|
||||
cfg.GoogleSkipOrgRoleSync = true
|
||||
cfg.GoogleSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(true)
|
||||
}
|
||||
|
||||
func readAuthGitlabSettings(cfg *Cfg) {
|
||||
|
Loading…
Reference in New Issue
Block a user