mirror of
https://github.com/grafana/grafana.git
synced 2024-11-21 16:38:03 -06:00
OIDC: Support Generic OAuth org to role mappings (#87394)
* Social: link to OrgRoleMapper * OIDC: support Generic Oauth org to role mappings Fixes: #73448 Signed-off-by: Mathieu Parent <math.parent@gmail.com> * Handle when getAllOrgs fails in the org_role_mapper * Add more tests * OIDC: ensure orgs are evaluated from API when not from token Signed-off-by: Mathieu Parent <math.parent@gmail.com> * OIDC: ensure AutoAssignOrg is applied with OrgMapping without RoleAttributeStrict Signed-off-by: Mathieu Parent <math.parent@gmail.com> * Extend docs * Fix test, lint --------- Signed-off-by: Mathieu Parent <math.parent@gmail.com> Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
parent
42126398be
commit
b8c9ae0eb7
@ -807,6 +807,8 @@ login_attribute_path =
|
||||
name_attribute_path =
|
||||
role_attribute_path =
|
||||
role_attribute_strict = false
|
||||
org_attribute_path =
|
||||
org_mapping =
|
||||
groups_attribute_path =
|
||||
id_token_attribute_name =
|
||||
team_ids_attribute_path =
|
||||
|
@ -742,6 +742,8 @@
|
||||
;allowed_organizations =
|
||||
;role_attribute_path =
|
||||
;role_attribute_strict = false
|
||||
;org_attribute_path =
|
||||
;org_mapping =
|
||||
;groups_attribute_path =
|
||||
;team_ids_attribute_path =
|
||||
;tls_skip_verify_insecure = false
|
||||
|
@ -206,6 +206,8 @@ If no valid role is found, the user is assigned the role specified by [the `auto
|
||||
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.
|
||||
|
||||
You can use the `org_attribute_path` and `org_mapping` configuration options to add roles to other orgs. For more information, refer to [Org roles mapping examples]({{< relref "#org-roles-mapping-examples" >}}). If both org role mapping (`org_mapping`) and the regular role mapping (`role_attribute_path`) are specified, then the user will get the highest of the two mapped roles.
|
||||
|
||||
To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads.
|
||||
|
||||
#### Role mapping examples
|
||||
@ -296,6 +298,43 @@ role_attribute_path = "'Viewer'"
|
||||
skip_org_role_sync = false
|
||||
```
|
||||
|
||||
#### Org roles mapping examples
|
||||
|
||||
This section includes examples of configuration settings used for org to role mapping.
|
||||
|
||||
##### Map organization roles
|
||||
|
||||
In this example, the user has been granted the role of a `Viewer` in the `org_foo` org, and the role of an `Editor` in the `org_bar` and `org_baz` orgs.
|
||||
|
||||
If the user was a member of the `admin` group, they would be granted the Grafana server administrator role.
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"info": {
|
||||
...
|
||||
"roles": [
|
||||
"org_foo",
|
||||
"org_bar",
|
||||
"another_org'
|
||||
],
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Config:
|
||||
|
||||
```ini
|
||||
role_attribute_path = contains(info.roles[*], 'admin') && 'GrafanaAdmin' || 'None'
|
||||
allow_assign_grafana_admin = true
|
||||
org_attribute_path = info.roles
|
||||
org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor
|
||||
```
|
||||
|
||||
### Configure team synchronization
|
||||
|
||||
> **Note:** Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud/).
|
||||
@ -358,7 +397,9 @@ The following table outlines the various generic OAuth2 configuration options. Y
|
||||
| `email_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address]({{< relref "#configure-email-address" >}}). | |
|
||||
| `email_attribute_name` | No | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address]({{< relref "#configure-email-address" >}}). | `email:primary` |
|
||||
| `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 OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo 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 org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
|
||||
| `org_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping examples]({{< relref "#org-roles-mapping-examples" >}}). | |
|
||||
| `org_mapping` | No | List of comma- or space-separated Value:OrgIdOrName:Role mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `Viewer`, `Editor` or `Admin`. For more information on org to role mapping, refer to [Org roles mapping examples]({{< relref "#org-roles-mapping-examples" >}}). | |
|
||||
| `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. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
|
||||
| `groups_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | |
|
||||
|
@ -78,7 +78,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
||||
PluginSettings: cfg.PluginSettings,
|
||||
}),
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), &ssosettingstests.MockService{}),
|
||||
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
|
||||
}
|
||||
|
||||
m := web.New()
|
||||
|
@ -78,9 +78,9 @@ type keySetJWKS struct {
|
||||
jose.JSONWebKeySet
|
||||
}
|
||||
|
||||
func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) *SocialAzureAD {
|
||||
func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) *SocialAzureAD {
|
||||
provider := &SocialAzureAD{
|
||||
SocialBase: newSocialBase(social.AzureADProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.AzureADProviderName, orgRoleMapper, info, features, cfg),
|
||||
cache: cache,
|
||||
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
|
||||
forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], ExtraAzureADSettingKeys[forceUseGraphAPIKey].DefaultValue.(bool)),
|
||||
@ -179,7 +179,7 @@ func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettin
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.AzureADProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.AzureADProviderName, newInfo)
|
||||
|
||||
if newInfo.UseRefreshToken {
|
||||
appendUniqueScope(s.Config, social.OfflineAccessScope)
|
||||
|
@ -711,7 +711,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
|
||||
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
|
||||
|
||||
if tt.fields.usGovURL {
|
||||
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
||||
@ -884,7 +884,7 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
|
||||
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
|
||||
|
||||
s.SocialBase.Endpoint.AuthURL = authURL
|
||||
|
||||
@ -982,7 +982,7 @@ func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewAzureADProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
|
||||
s := NewAzureADProvider(tc.settings, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
|
||||
|
||||
require.Equal(t, tc.want.forceUseGraphAPI, s.forceUseGraphAPI)
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
@ -1125,7 +1125,7 @@ func TestSocialAzureAD_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewAzureADProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
|
||||
s := NewAzureADProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -1205,7 +1205,7 @@ func TestSocialAzureAD_Reload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewAzureADProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
|
||||
s := NewAzureADProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
@ -1262,7 +1262,7 @@ func TestSocialAzureAD_Reload_ExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewAzureADProvider(tc.info, setting.NewCfg(), &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), remotecache.FakeCacheStorage{})
|
||||
s := NewAzureADProvider(tc.info, setting.NewCfg(), nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), remotecache.FakeCacheStorage{})
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
require.NoError(t, err)
|
||||
|
@ -53,9 +53,9 @@ type SocialGenericOAuth struct {
|
||||
teamIds []string
|
||||
}
|
||||
|
||||
func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGenericOAuth {
|
||||
func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGenericOAuth {
|
||||
provider := &SocialGenericOAuth{
|
||||
SocialBase: newSocialBase(social.GenericOAuthProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.GenericOAuthProviderName, orgRoleMapper, info, features, cfg),
|
||||
teamsUrl: info.TeamsUrl,
|
||||
emailAttributeName: info.EmailAttributeName,
|
||||
emailAttributePath: info.EmailAttributePath,
|
||||
@ -122,7 +122,7 @@ func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOS
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.GenericOAuthProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.GenericOAuthProviderName, newInfo)
|
||||
|
||||
s.teamsUrl = newInfo.TeamsUrl
|
||||
s.emailAttributeName = newInfo.EmailAttributeName
|
||||
@ -231,6 +231,7 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
|
||||
}
|
||||
|
||||
userInfo := &social.BasicUserInfo{}
|
||||
var externalOrgs []string
|
||||
for _, data := range toCheck {
|
||||
s.log.Debug("Processing external user info", "source", data.source, "data", data)
|
||||
|
||||
@ -265,6 +266,15 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
|
||||
}
|
||||
}
|
||||
|
||||
if len(externalOrgs) == 0 && !s.info.SkipOrgRoleSync {
|
||||
var err error
|
||||
externalOrgs, err = s.extractOrgs(data.rawJSON)
|
||||
if err != nil {
|
||||
s.log.Warn("Failed to extract orgs", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(userInfo.Groups) == 0 {
|
||||
groups, err := s.extractGroups(data)
|
||||
if err != nil {
|
||||
@ -276,11 +286,14 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
|
||||
}
|
||||
}
|
||||
|
||||
if userInfo.Role == "" && !s.info.SkipOrgRoleSync {
|
||||
if s.info.RoleAttributeStrict {
|
||||
return nil, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute")
|
||||
if !s.info.SkipOrgRoleSync {
|
||||
userInfo.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, externalOrgs, userInfo.Role)
|
||||
if s.info.RoleAttributeStrict && len(userInfo.OrgRoles) == 0 {
|
||||
// If no roles are found and role_attribute_strict is set, return an error.
|
||||
// The s.info.RoleAttributeStrict is necessary, because there is a case when len(userInfo.OrgRoles) == 0,
|
||||
// but strict role mapping is not enabled (when getAllOrgs fails).
|
||||
return nil, errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data")
|
||||
}
|
||||
userInfo.Role = s.defaultRole()
|
||||
}
|
||||
|
||||
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync {
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
|
||||
@ -23,22 +24,20 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
EmailAttributePath: "email",
|
||||
}, &setting.Cfg{},
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
tests := []struct {
|
||||
func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
SkipOrgRoleSync bool
|
||||
AllowAssignGrafanaAdmin bool
|
||||
ResponseBody any
|
||||
OAuth2Extra any
|
||||
Setup func(*orgtest.FakeOrgService)
|
||||
RoleAttributePath string
|
||||
RoleAttributeStrict bool
|
||||
OrgAttributePath string
|
||||
OrgMapping []string
|
||||
ExpectedEmail string
|
||||
ExpectedRole org.RoleType
|
||||
ExpectedOrgRoles map[int64]org.RoleType
|
||||
ExpectedError error
|
||||
ExpectedGrafanaAdmin *bool
|
||||
}{
|
||||
@ -50,7 +49,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, no role path, no API response, use id_token",
|
||||
@ -60,7 +59,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, an invalid role path, no API response, use id_token",
|
||||
@ -70,7 +69,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "invalid_path",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid role path, a valid API response, use API response",
|
||||
@ -80,7 +79,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, no role path, a valid API response, use API response",
|
||||
@ -89,7 +88,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a role path, a valid API response without a role, use API response",
|
||||
@ -98,13 +97,13 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid role path, no API response, no data",
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, a valid role path, a valid API response, prefer id_token",
|
||||
@ -118,7 +117,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token and AssignGrafanaAdmin is unchecked, don't grant Server Admin",
|
||||
@ -133,8 +132,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedGrafanaAdmin: nil,
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token and AssignGrafanaAdmin is checked, grant Server Admin",
|
||||
@ -149,8 +148,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedGrafanaAdmin: trueBoolPtr(),
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token",
|
||||
@ -164,7 +163,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "invalid_path",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token with no email, a valid role path, a valid API response with no role, merge",
|
||||
@ -177,7 +176,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "from_response@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token with no role, a valid role path, a valid API response with no email, merge",
|
||||
@ -190,8 +189,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "role",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Viewer",
|
||||
ExpectedError: nil,
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role",
|
||||
@ -202,10 +201,10 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "contains(info.roles[*], 'dev') && 'Editor'",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Editor",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct role using the userinfo API response (JMESPath warning on id_token)",
|
||||
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct org roles using the userinfo API response (JMESPath warning on id_token)",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
||||
@ -217,7 +216,22 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin'",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Admin",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid API response, derive the correct org roles using the userinfo API response (JMESPath warning on id_token)",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
||||
},
|
||||
ResponseBody: map[string]any{
|
||||
"info": map[string]any{
|
||||
"roles": []string{"engineering", "SRE"},
|
||||
},
|
||||
},
|
||||
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin'",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, a valid advanced JMESPath role path, a valid API response, prefer ID token",
|
||||
@ -233,7 +247,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "Editor",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Given skip org role sync set to true, with a valid id_token, a valid advanced JMESPath role path, a valid API response, no org role should be set",
|
||||
@ -250,17 +264,213 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
},
|
||||
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin' || contains(info.roles[*], 'dev') && 'Editor' || 'Viewer'",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedRole: "",
|
||||
ExpectedOrgRoles: nil,
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token without role info, a valid advanced JMESPath role path, a valid org attribute path, a valid org mapping, a valid API response, derive the correct org roles using the userinfo API response (JMESPath warning on id_token)",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
|
||||
},
|
||||
ResponseBody: map[string]any{
|
||||
"info": map[string]any{
|
||||
"roles": []string{"engineering", "SRE"},
|
||||
},
|
||||
},
|
||||
RoleAttributePath: "contains(info.roles[*], 'SRE') && 'Admin'",
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"SRE:2:Viewer", "engineering:3:Editor"},
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin, 3: org.RoleAdmin},
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, a role attribute path, an org roles path, an org mapping, a valid API response, prefer ID token",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "'Viewer'",
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Should not fail when the evaluated role is invalid, role_attribute_strict is set to true and evaluated org roles are not empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "'Invalid'",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Should not fail when the evaluated role is valid, role_attribute_strict is set to true and evaluated org roles are empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "'Editor'",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"notmatching:org_dev:Viewer", "notmatching:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Should not fail when role_attribute path is empty, role_attribute_strict is set to true and evaluated org roles are not empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Should return empty when evaluated role is empty/invalid, role_attribute_strict is set to false and evaluated org roles are empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
Setup: func(orgSvc *orgtest.FakeOrgService) {
|
||||
orgSvc.ExpectedError = assert.AnError
|
||||
},
|
||||
RoleAttributePath: "'Invalid'",
|
||||
RoleAttributeStrict: false,
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"dev:*:Viewer"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: nil,
|
||||
},
|
||||
{
|
||||
Name: "Should fail when role_attribute_path is empty, role_attribute_strict is set to true and org_mapping is empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "info.invalid",
|
||||
OrgMapping: []string{},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedError: errRoleAttributeStrictViolation,
|
||||
},
|
||||
{
|
||||
Name: "Should fail when role_attribute_path evaluates to invalid role, role_attribute_strict is set to true and org_mapping is empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "'Invalid'",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "info.invalid",
|
||||
OrgMapping: []string{},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedError: errRoleAttributeStrictViolation,
|
||||
},
|
||||
{
|
||||
Name: "Should fail when role_attribute path is empty, role_attribute_strict is set to true and evaluated org roles are empty",
|
||||
SkipOrgRoleSync: false,
|
||||
AllowAssignGrafanaAdmin: false,
|
||||
ResponseBody: map[string]any{"info": map[string]any{"roles": []string{"engineering", "SRE"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "info.invalid",
|
||||
OrgMapping: []string{"dev:org_dev:Viewer", "engineering:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedError: errRoleAttributeStrictViolation,
|
||||
},
|
||||
{
|
||||
Name: "Should get orgs from API when not in token",
|
||||
ResponseBody: map[string]any{"anotherInfo": map[string]any{"roles": []string{"fromApiOne", "fromApiTwo"}}},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "",
|
||||
RoleAttributeStrict: true,
|
||||
OrgAttributePath: "anotherInfo.roles",
|
||||
OrgMapping: []string{"fromApiOne:org_dev:Viewer", "fromApiTwo:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleViewer, 5: org.RoleEditor},
|
||||
},
|
||||
{
|
||||
Name: "Give AutoAssignOrgRole in AutoAssignOrgId when OrgMapping returns no OrgRoles",
|
||||
ResponseBody: map[string]any{},
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "email": "john.doe@example.com",
|
||||
// "info": { "roles": [ "dev", "engineering" ] }}
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiaW5mbyI6eyJyb2xlcyI6WyJkZXYiLCJlbmdpbmVlcmluZyJdfX0.RmmQfv25eXb4p3wMrJsvXfGQ6EXhGtwRXo6SlCFHRNg"},
|
||||
RoleAttributePath: "",
|
||||
OrgAttributePath: "info.roles",
|
||||
OrgMapping: []string{"foo:org_dev:Viewer", "bar:org_engineering:Editor"},
|
||||
ExpectedEmail: "john.doe@example.com",
|
||||
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
provider.info.RoleAttributePath = test.RoleAttributePath
|
||||
provider.info.AllowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin
|
||||
provider.info.SkipOrgRoleSync = test.SkipOrgRoleSync
|
||||
cfg := &setting.Cfg{
|
||||
AutoAssignOrg: true,
|
||||
AutoAssignOrgId: 2,
|
||||
AutoAssignOrgRole: string(org.RoleViewer),
|
||||
}
|
||||
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
body, err := json.Marshal(test.ResponseBody)
|
||||
for _, tc := range testCases {
|
||||
orgSvc := &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "org_dev"}, {ID: 5, Name: "org_engineering"}}}
|
||||
if tc.Setup != nil {
|
||||
tc.Setup(orgSvc)
|
||||
}
|
||||
orgRoleMapper := ProvideOrgRoleMapper(cfg, orgSvc)
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
EmailAttributePath: "email",
|
||||
}, cfg,
|
||||
orgRoleMapper,
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
provider.info.RoleAttributePath = tc.RoleAttributePath
|
||||
provider.info.OrgAttributePath = tc.OrgAttributePath
|
||||
provider.info.OrgMapping = tc.OrgMapping
|
||||
provider.orgMappingCfg = orgRoleMapper.ParseOrgMappingSettings(context.Background(), tc.OrgMapping, tc.RoleAttributeStrict)
|
||||
provider.info.AllowAssignGrafanaAdmin = tc.AllowAssignGrafanaAdmin
|
||||
provider.info.SkipOrgRoleSync = tc.SkipOrgRoleSync
|
||||
provider.info.RoleAttributeStrict = tc.RoleAttributeStrict
|
||||
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
body, err := json.Marshal(tc.ResponseBody)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@ -276,283 +486,288 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
|
||||
token := staticToken.WithExtra(test.OAuth2Extra)
|
||||
token := staticToken.WithExtra(tc.OAuth2Extra)
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
if test.ExpectedError != nil {
|
||||
require.ErrorIs(t, err, test.ExpectedError)
|
||||
if tc.ExpectedError != nil {
|
||||
require.ErrorIs(t, err, tc.ExpectedError)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.ExpectedEmail, actualResult.Email)
|
||||
require.Equal(t, test.ExpectedEmail, actualResult.Login)
|
||||
require.Equal(t, test.ExpectedRole, actualResult.Role)
|
||||
require.Equal(t, test.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
|
||||
require.Equal(t, tc.ExpectedEmail, actualResult.Email)
|
||||
require.Equal(t, tc.ExpectedEmail, actualResult.Login)
|
||||
require.Equal(t, tc.ExpectedOrgRoles, actualResult.OrgRoles)
|
||||
require.Equal(t, tc.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserInfoSearchesForLogin(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
Extra: map[string]string{
|
||||
"login_attribute_path": "login",
|
||||
testCases := []struct {
|
||||
Name string
|
||||
ResponseBody any
|
||||
OAuth2Extra any
|
||||
LoginAttributePath string
|
||||
ExpectedLogin string
|
||||
}{
|
||||
{
|
||||
Name: "Given a valid id_token, a valid login path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
|
||||
},
|
||||
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
LoginAttributePath: "role",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, no login path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
|
||||
},
|
||||
LoginAttributePath: "",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid login path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"user_uid": "johndoe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
LoginAttributePath: "user_uid",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, no login path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"login": "johndoe",
|
||||
},
|
||||
LoginAttributePath: "",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a login path, a valid API response without a login, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"username": "john.doe",
|
||||
},
|
||||
LoginAttributePath: "login",
|
||||
ExpectedLogin: "john.doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid login path, no API response, no data",
|
||||
LoginAttributePath: "login",
|
||||
ExpectedLogin: "",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
ResponseBody any
|
||||
OAuth2Extra any
|
||||
LoginAttributePath string
|
||||
ExpectedLogin string
|
||||
}{
|
||||
{
|
||||
Name: "Given a valid id_token, a valid login path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
|
||||
},
|
||||
LoginAttributePath: "role",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, no login path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8",
|
||||
},
|
||||
LoginAttributePath: "",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid login path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"user_uid": "johndoe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
LoginAttributePath: "user_uid",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, no login path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"login": "johndoe",
|
||||
},
|
||||
LoginAttributePath: "",
|
||||
ExpectedLogin: "johndoe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a login path, a valid API response without a login, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"username": "john.doe",
|
||||
},
|
||||
LoginAttributePath: "login",
|
||||
ExpectedLogin: "john.doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid login path, no API response, no data",
|
||||
LoginAttributePath: "login",
|
||||
ExpectedLogin: "",
|
||||
},
|
||||
}
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
Extra: map[string]string{
|
||||
"login_attribute_path": "login",
|
||||
},
|
||||
}, setting.NewCfg(),
|
||||
ProvideOrgRoleMapper(setting.NewCfg(), orgtest.NewOrgServiceFake()),
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
for _, test := range tests {
|
||||
provider.loginAttributePath = test.LoginAttributePath
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
body, err := json.Marshal(test.ResponseBody)
|
||||
for _, tc := range testCases {
|
||||
provider.loginAttributePath = tc.LoginAttributePath
|
||||
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
body, err := json.Marshal(tc.ResponseBody)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
t.Log("Writing fake API response body", "body", tc.ResponseBody)
|
||||
_, err = w.Write(body)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
t.Log("Writing fake API response body", "body", test.ResponseBody)
|
||||
_, err = w.Write(body)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
provider.info.ApiUrl = ts.URL
|
||||
staticToken := oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
}))
|
||||
provider.info.ApiUrl = ts.URL
|
||||
staticToken := oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
|
||||
token := staticToken.WithExtra(test.OAuth2Extra)
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.ExpectedLogin, actualResult.Login)
|
||||
})
|
||||
}
|
||||
})
|
||||
token := staticToken.WithExtra(tc.OAuth2Extra)
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.ExpectedLogin, actualResult.Login)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserInfoSearchesForName(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
Extra: map[string]string{
|
||||
"name_attribute_path": "name",
|
||||
testCases := []struct {
|
||||
Name string
|
||||
ResponseBody any
|
||||
OAuth2Extra any
|
||||
NameAttributePath string
|
||||
ExpectedName string
|
||||
}{
|
||||
{
|
||||
Name: "Given a valid id_token, a valid name path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
|
||||
},
|
||||
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
NameAttributePath: "name",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, no name path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
|
||||
},
|
||||
NameAttributePath: "",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid name path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"user_name": "John Doe",
|
||||
"login": "johndoe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
NameAttributePath: "user_name",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, no name path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"display_name": "John Doe",
|
||||
"login": "johndoe",
|
||||
},
|
||||
NameAttributePath: "",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a name path, a valid API response without a name, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"display_name": "John Doe",
|
||||
"username": "john.doe",
|
||||
},
|
||||
NameAttributePath: "name",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid name path, no API response, no data",
|
||||
NameAttributePath: "name",
|
||||
ExpectedName: "",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
ResponseBody any
|
||||
OAuth2Extra any
|
||||
NameAttributePath string
|
||||
ExpectedName string
|
||||
}{
|
||||
{
|
||||
Name: "Given a valid id_token, a valid name path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
|
||||
},
|
||||
NameAttributePath: "name",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given a valid id_token, no name path, no API response, use id_token",
|
||||
OAuth2Extra: map[string]any{
|
||||
// { "name": "John Doe", "login": "johndoe", "email": "john.doe@example.com" }
|
||||
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIn0.oMsXH0mHxUSYMXh6FonZIWh8LgNIcYbKRLSO1bwnfSI",
|
||||
},
|
||||
NameAttributePath: "",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid name path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"user_name": "John Doe",
|
||||
"login": "johndoe",
|
||||
"email": "john.doe@example.com",
|
||||
},
|
||||
NameAttributePath: "user_name",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, no name path, a valid API response, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"display_name": "John Doe",
|
||||
"login": "johndoe",
|
||||
},
|
||||
NameAttributePath: "",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a name path, a valid API response without a name, use API response",
|
||||
ResponseBody: map[string]any{
|
||||
"display_name": "John Doe",
|
||||
"username": "john.doe",
|
||||
},
|
||||
NameAttributePath: "name",
|
||||
ExpectedName: "John Doe",
|
||||
},
|
||||
{
|
||||
Name: "Given no id_token, a valid name path, no API response, no data",
|
||||
NameAttributePath: "name",
|
||||
ExpectedName: "",
|
||||
},
|
||||
}
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
Extra: map[string]string{
|
||||
"name_attribute_path": "name",
|
||||
},
|
||||
},
|
||||
setting.NewCfg(),
|
||||
ProvideOrgRoleMapper(setting.NewCfg(), orgtest.NewOrgServiceFake()),
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
for _, test := range tests {
|
||||
provider.nameAttributePath = test.NameAttributePath
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
body, err := json.Marshal(test.ResponseBody)
|
||||
for _, tc := range testCases {
|
||||
provider.nameAttributePath = tc.NameAttributePath
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
body, err := json.Marshal(tc.ResponseBody)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
t.Log("Writing fake API response body", "body", tc.ResponseBody)
|
||||
_, err = w.Write(body)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
t.Log("Writing fake API response body", "body", test.ResponseBody)
|
||||
_, err = w.Write(body)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
provider.info.ApiUrl = ts.URL
|
||||
staticToken := oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
}))
|
||||
provider.info.ApiUrl = ts.URL
|
||||
staticToken := oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
|
||||
token := staticToken.WithExtra(test.OAuth2Extra)
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.ExpectedName, actualResult.Name)
|
||||
})
|
||||
}
|
||||
})
|
||||
token := staticToken.WithExtra(tc.OAuth2Extra)
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.ExpectedName, actualResult.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserInfoSearchesForGroup(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
groupsAttributePath string
|
||||
responseBody any
|
||||
expectedResult []string
|
||||
}{
|
||||
{
|
||||
name: "If groups are not set, user groups are nil",
|
||||
groupsAttributePath: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
name: "If groups are empty, user groups are nil",
|
||||
groupsAttributePath: "info.groups",
|
||||
responseBody: map[string]any{
|
||||
"info": map[string]any{
|
||||
"groups": []string{},
|
||||
},
|
||||
testCases := []struct {
|
||||
name string
|
||||
groupsAttributePath string
|
||||
responseBody any
|
||||
expectedResult []string
|
||||
}{
|
||||
{
|
||||
name: "If groups are not set, user groups are nil",
|
||||
groupsAttributePath: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
name: "If groups are empty, user groups are nil",
|
||||
groupsAttributePath: "info.groups",
|
||||
responseBody: map[string]any{
|
||||
"info": map[string]any{
|
||||
"groups": []string{},
|
||||
},
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
name: "If groups are set, user groups are set",
|
||||
groupsAttributePath: "info.groups",
|
||||
responseBody: map[string]any{
|
||||
"info": map[string]any{
|
||||
"groups": []string{"foo", "bar"},
|
||||
},
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
name: "If groups are set, user groups are set",
|
||||
groupsAttributePath: "info.groups",
|
||||
responseBody: map[string]any{
|
||||
"info": map[string]any{
|
||||
"groups": []string{"foo", "bar"},
|
||||
},
|
||||
expectedResult: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
expectedResult: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
body, err := json.Marshal(test.responseBody)
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
body, err := json.Marshal(test.responseBody)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
t.Log("Writing fake API response body", "body", test.responseBody)
|
||||
_, err := w.Write(body)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
t.Log("Writing fake API response body", "body", test.responseBody)
|
||||
_, err := w.Write(body)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
}))
|
||||
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
GroupsAttributePath: test.groupsAttributePath,
|
||||
ApiUrl: ts.URL,
|
||||
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
GroupsAttributePath: test.groupsAttributePath,
|
||||
ApiUrl: ts.URL,
|
||||
}, setting.NewCfg(),
|
||||
ProvideOrgRoleMapper(setting.NewCfg(), orgtest.NewOrgServiceFake()),
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Now(),
|
||||
}
|
||||
|
||||
userInfo, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expectedResult, userInfo.Groups)
|
||||
})
|
||||
}
|
||||
})
|
||||
userInfo, err := provider.UserInfo(context.Background(), ts.Client(), token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expectedResult, userInfo.Groups)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayloadCompression(t *testing.T) {
|
||||
provider := NewGenericOAuthProvider(&social.OAuthInfo{
|
||||
EmailAttributePath: "email",
|
||||
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@ -707,7 +922,7 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
require.Equal(t, tc.want.nameAttributePath, s.nameAttributePath)
|
||||
require.Equal(t, tc.want.loginAttributePath, s.loginAttributePath)
|
||||
@ -870,7 +1085,7 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGenericOAuthProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGenericOAuthProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -950,7 +1165,7 @@ func TestSocialGenericOAuth_Reload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGenericOAuthProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGenericOAuthProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
@ -1048,7 +1263,7 @@ func TestGenericOAuth_Reload_ExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGenericOAuthProvider(tc.info, setting.NewCfg(), &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGenericOAuthProvider(tc.info, setting.NewCfg(), nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
require.NoError(t, err)
|
||||
|
@ -61,12 +61,12 @@ var (
|
||||
"User is not a member of one of the required organizations. Please contact identity provider administrator."))
|
||||
)
|
||||
|
||||
func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGithub {
|
||||
func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGithub {
|
||||
teamIdsSplitted := util.SplitString(info.Extra[teamIdsKey])
|
||||
teamIds := mustInts(teamIdsSplitted)
|
||||
|
||||
provider := &SocialGithub{
|
||||
SocialBase: newSocialBase(social.GitHubProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.GitHubProviderName, orgRoleMapper, info, features, cfg),
|
||||
teamIds: teamIds,
|
||||
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
|
||||
}
|
||||
@ -127,7 +127,7 @@ func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSetting
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.GitHubProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.GitHubProviderName, newInfo)
|
||||
|
||||
s.teamIds = teamIds
|
||||
s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey])
|
||||
|
@ -286,7 +286,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
|
||||
Extra: tt.oAuthExtraInfo,
|
||||
}, &setting.Cfg{
|
||||
AutoAssignOrgRole: tt.autoAssignOrgRole,
|
||||
}, &ssosettingstests.MockService{},
|
||||
}, nil, &ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
token := &oauth2.Token{
|
||||
@ -368,7 +368,7 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGitHubProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitHubProvider(tc.settings, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
require.Equal(t, tc.want.teamIds, s.teamIds)
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
@ -495,7 +495,7 @@ func TestSocialGitHub_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGitHubProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitHubProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -576,7 +576,7 @@ func TestSocialGitHub_Reload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGitHubProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitHubProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
@ -635,7 +635,7 @@ func TestGitHub_Reload_ExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGitHubProvider(tc.info, setting.NewCfg(), &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitHubProvider(tc.info, setting.NewCfg(), nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
require.NoError(t, err)
|
||||
|
@ -54,9 +54,9 @@ type userData struct {
|
||||
IsGrafanaAdmin *bool `json:"-"`
|
||||
}
|
||||
|
||||
func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGitlab {
|
||||
func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGitlab {
|
||||
provider := &SocialGitlab{
|
||||
SocialBase: newSocialBase(social.GitlabProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.GitlabProviderName, orgRoleMapper, info, features, cfg),
|
||||
}
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
|
||||
@ -92,7 +92,7 @@ func (s *SocialGitlab) Reload(ctx context.Context, settings ssoModels.SSOSetting
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.GitlabProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.GitlabProviderName, newInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ const (
|
||||
func TestSocialGitlab_UserInfo(t *testing.T) {
|
||||
var nilPointer *bool
|
||||
|
||||
provider := NewGitLabProvider(&social.OAuthInfo{SkipOrgRoleSync: false}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
provider := NewGitLabProvider(&social.OAuthInfo{SkipOrgRoleSync: false}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
type conf struct {
|
||||
AllowAssignGrafanaAdmin bool
|
||||
@ -363,7 +363,7 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
|
||||
},
|
||||
&setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
}, &ssosettingstests.MockService{},
|
||||
}, nil, &ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
// Test case: successful extraction
|
||||
@ -454,7 +454,7 @@ func TestSocialGitlab_GetGroupsNextPage(t *testing.T) {
|
||||
defer mockServer.Close()
|
||||
|
||||
// Create a SocialGitlab instance with the mock server URL
|
||||
s := NewGitLabProvider(&social.OAuthInfo{ApiUrl: mockServer.URL}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitLabProvider(&social.OAuthInfo{ApiUrl: mockServer.URL}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
// Call getGroups and verify that it returns all groups
|
||||
expectedGroups := []string{"admins", "editors", "viewers", "serveradmins"}
|
||||
@ -576,7 +576,7 @@ func TestSocialGitlab_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGitLabProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitLabProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -657,7 +657,7 @@ func TestSocialGitlab_Reload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGitLabProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGitLabProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
|
@ -48,9 +48,9 @@ type googleUserData struct {
|
||||
rawJSON []byte `json:"-"`
|
||||
}
|
||||
|
||||
func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGoogle {
|
||||
func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGoogle {
|
||||
provider := &SocialGoogle{
|
||||
SocialBase: newSocialBase(social.GoogleProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.GoogleProviderName, orgRoleMapper, info, features, cfg),
|
||||
validateHD: MustBool(info.Extra[validateHDKey], true),
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSetting
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.GoogleProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.GoogleProviderName, newInfo)
|
||||
s.validateHD = MustBool(newInfo.Extra[validateHDKey], true)
|
||||
|
||||
return nil
|
||||
|
@ -202,6 +202,7 @@ func TestSocialGoogle_retrieveGroups(t *testing.T) {
|
||||
&setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
nil,
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
@ -654,6 +655,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
SkipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||
},
|
||||
&setting.Cfg{},
|
||||
nil,
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
@ -795,7 +797,7 @@ func TestSocialGoogle_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGoogleProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGoogleProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -876,7 +878,7 @@ func TestSocialGoogle_Reload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGoogleProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGoogleProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
@ -929,7 +931,7 @@ func TestIsHDAllowed(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
info := &social.OAuthInfo{}
|
||||
info.AllowedDomains = tc.allowedDomains
|
||||
s := NewGoogleProvider(info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGoogleProvider(info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s.validateHD = tc.validateHD
|
||||
err := s.isHDAllowed(tc.email)
|
||||
|
||||
|
@ -37,14 +37,14 @@ type OrgRecord struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGrafanaCom {
|
||||
func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGrafanaCom {
|
||||
// Override necessary settings
|
||||
info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize"
|
||||
info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token"
|
||||
info.AuthStyle = "inheader"
|
||||
|
||||
provider := &SocialGrafanaCom{
|
||||
SocialBase: newSocialBase(social.GrafanaComProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg),
|
||||
url: cfg.GrafanaComURL,
|
||||
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
|
||||
}
|
||||
@ -87,7 +87,7 @@ func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSet
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.GrafanaComProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.GrafanaComProviderName, newInfo)
|
||||
|
||||
s.url = s.cfg.GrafanaComURL
|
||||
s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey])
|
||||
|
@ -32,7 +32,7 @@ const (
|
||||
)
|
||||
|
||||
func TestSocialGrafanaCom_UserInfo(t *testing.T) {
|
||||
provider := NewGrafanaComProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
provider := NewGrafanaComProvider(social.NewOAuthInfo(), &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
type conf struct {
|
||||
skipOrgRoleSync bool
|
||||
@ -130,7 +130,7 @@ func TestSocialGrafanaCom_InitializeExtraFields(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGrafanaComProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGrafanaComProvider(tc.settings, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
})
|
||||
@ -199,7 +199,7 @@ func TestSocialGrafanaCom_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewGrafanaComProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGrafanaComProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -299,7 +299,7 @@ func TestSocialGrafanaCom_Reload(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
GrafanaComURL: GrafanaComURL,
|
||||
}
|
||||
s := NewGrafanaComProvider(tc.info, cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGrafanaComProvider(tc.info, cfg, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
@ -360,7 +360,7 @@ func TestSocialGrafanaCom_Reload_ExtraFields(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
GrafanaComURL: GrafanaComURL,
|
||||
}
|
||||
s := NewGrafanaComProvider(tc.info, cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewGrafanaComProvider(tc.info, cfg, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
require.NoError(t, err)
|
||||
|
@ -46,9 +46,9 @@ type OktaClaims struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialOkta {
|
||||
func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialOkta {
|
||||
provider := &SocialOkta{
|
||||
SocialBase: newSocialBase(social.OktaProviderName, info, features, cfg),
|
||||
SocialBase: newSocialBase(social.OktaProviderName, orgRoleMapper, info, features, cfg),
|
||||
}
|
||||
|
||||
if info.UseRefreshToken {
|
||||
@ -88,7 +88,7 @@ func (s *SocialOkta) Reload(ctx context.Context, settings ssoModels.SSOSettings)
|
||||
s.reloadMutex.Lock()
|
||||
defer s.reloadMutex.Unlock()
|
||||
|
||||
s.updateInfo(social.OktaProviderName, newInfo)
|
||||
s.updateInfo(ctx, social.OktaProviderName, newInfo)
|
||||
if newInfo.UseRefreshToken {
|
||||
appendUniqueScope(s.Config, social.OfflineAccessScope)
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ func TestSocialOkta_UserInfo(t *testing.T) {
|
||||
&setting.Cfg{
|
||||
AutoAssignOrgRole: tt.autoAssignOrgRole,
|
||||
},
|
||||
nil,
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
@ -281,7 +282,7 @@ func TestSocialOkta_Validate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewOktaProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewOktaProvider(&social.OAuthInfo{}, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
if tc.requester == nil {
|
||||
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
|
||||
@ -361,7 +362,7 @@ func TestSocialOkta_Reload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewOktaProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
s := NewOktaProvider(tc.info, &setting.Cfg{}, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
|
||||
|
||||
err := s.Reload(context.Background(), tc.settings)
|
||||
if tc.expectError {
|
||||
|
@ -29,32 +29,39 @@ import (
|
||||
|
||||
type SocialBase struct {
|
||||
*oauth2.Config
|
||||
info *social.OAuthInfo
|
||||
cfg *setting.Cfg
|
||||
reloadMutex sync.RWMutex
|
||||
log log.Logger
|
||||
features featuremgmt.FeatureToggles
|
||||
info *social.OAuthInfo
|
||||
cfg *setting.Cfg
|
||||
reloadMutex sync.RWMutex
|
||||
log log.Logger
|
||||
features featuremgmt.FeatureToggles
|
||||
orgRoleMapper *OrgRoleMapper
|
||||
orgMappingCfg *MappingConfiguration
|
||||
}
|
||||
|
||||
func newSocialBase(name string,
|
||||
orgRoleMapper *OrgRoleMapper,
|
||||
info *social.OAuthInfo,
|
||||
features featuremgmt.FeatureToggles,
|
||||
cfg *setting.Cfg,
|
||||
|
||||
) *SocialBase {
|
||||
logger := log.New("oauth." + name)
|
||||
|
||||
return &SocialBase{
|
||||
Config: createOAuthConfig(info, cfg, name),
|
||||
info: info,
|
||||
log: logger,
|
||||
features: features,
|
||||
cfg: cfg,
|
||||
Config: createOAuthConfig(info, cfg, name),
|
||||
info: info,
|
||||
log: logger,
|
||||
features: features,
|
||||
cfg: cfg,
|
||||
orgRoleMapper: orgRoleMapper,
|
||||
orgMappingCfg: orgRoleMapper.ParseOrgMappingSettings(context.Background(), info.OrgMapping, info.RoleAttributeStrict),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SocialBase) updateInfo(name string, info *social.OAuthInfo) {
|
||||
func (s *SocialBase) updateInfo(ctx context.Context, name string, info *social.OAuthInfo) {
|
||||
s.Config = createOAuthConfig(info, s.cfg, name)
|
||||
s.info = info
|
||||
s.orgMappingCfg = s.orgRoleMapper.ParseOrgMappingSettings(ctx, info.OrgMapping, info.RoleAttributeStrict)
|
||||
}
|
||||
|
||||
type groupStruct struct {
|
||||
@ -171,6 +178,14 @@ func (s *SocialBase) searchRole(rawJSON []byte, groups []string) (org.RoleType,
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (s *SocialBase) extractOrgs(rawJSON []byte) ([]string, error) {
|
||||
if s.info.OrgAttributePath == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return util.SearchJSONForStringSliceAttr(s.info.OrgAttributePath, rawJSON)
|
||||
}
|
||||
|
||||
// defaultRole returns the default role for the user based on the autoAssignOrgRole setting
|
||||
// if legacy is enabled "" is returned indicating the previous role assignment is used.
|
||||
func (s *SocialBase) defaultRole() org.RoleType {
|
||||
|
@ -74,6 +74,8 @@ type OAuthInfo struct {
|
||||
Name string `mapstructure:"name" toml:"name"`
|
||||
RoleAttributePath string `mapstructure:"role_attribute_path" toml:"role_attribute_path"`
|
||||
RoleAttributeStrict bool `mapstructure:"role_attribute_strict" toml:"role_attribute_strict"`
|
||||
OrgAttributePath string `mapstructure:"org_attribute_path"`
|
||||
OrgMapping []string `mapstructure:"org_mapping"`
|
||||
Scopes []string `mapstructure:"scopes" toml:"scopes"`
|
||||
SignoutRedirectUrl string `mapstructure:"signout_redirect_url" toml:"signout_redirect_url"`
|
||||
SkipOrgRoleSync bool `mapstructure:"skip_org_role_sync" toml:"skip_org_role_sync"`
|
||||
@ -104,11 +106,12 @@ type BasicUserInfo struct {
|
||||
Email string
|
||||
Login string
|
||||
Role org.RoleType
|
||||
OrgRoles map[int64]org.RoleType
|
||||
IsGrafanaAdmin *bool // nil will avoid overriding user's set server admin setting
|
||||
Groups []string
|
||||
}
|
||||
|
||||
func (b *BasicUserInfo) String() string {
|
||||
return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Role: %s, Groups: %v",
|
||||
b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups)
|
||||
return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Role: %s, Groups: %v, OrgRoles: %v",
|
||||
b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups, b.OrgRoles)
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ func ProvideService(cfg *setting.Cfg,
|
||||
usageStats usagestats.Service,
|
||||
bundleRegistry supportbundles.Service,
|
||||
cache remotecache.CacheStorage,
|
||||
orgRoleMapper *connectors.OrgRoleMapper,
|
||||
ssoSettings ssosettings.Service,
|
||||
) *SocialService {
|
||||
ss := &SocialService{
|
||||
@ -70,7 +71,7 @@ func ProvideService(cfg *setting.Cfg,
|
||||
continue
|
||||
}
|
||||
|
||||
conn, err := createOAuthConnector(ssoSetting.Provider, info, cfg, ssoSettings, features, cache)
|
||||
conn, err := createOAuthConnector(ssoSetting.Provider, info, cfg, orgRoleMapper, ssoSettings, features, cache)
|
||||
if err != nil {
|
||||
ss.log.Error("Failed to create OAuth provider", "error", err, "provider", ssoSetting.Provider)
|
||||
continue
|
||||
@ -98,7 +99,7 @@ func ProvideService(cfg *setting.Cfg,
|
||||
name = social.GrafanaComProviderName
|
||||
}
|
||||
|
||||
conn, _ := createOAuthConnector(name, info, cfg, ssoSettings, features, cache)
|
||||
conn, _ := createOAuthConnector(name, info, cfg, orgRoleMapper, ssoSettings, features, cache)
|
||||
|
||||
ss.socialMap[name] = conn
|
||||
}
|
||||
@ -229,22 +230,22 @@ func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]any, err
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func createOAuthConnector(name string, info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) (social.SocialConnector, error) {
|
||||
func createOAuthConnector(name string, info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *connectors.OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) (social.SocialConnector, error) {
|
||||
switch name {
|
||||
case social.AzureADProviderName:
|
||||
return connectors.NewAzureADProvider(info, cfg, ssoSettings, features, cache), nil
|
||||
return connectors.NewAzureADProvider(info, cfg, orgRoleMapper, ssoSettings, features, cache), nil
|
||||
case social.GenericOAuthProviderName:
|
||||
return connectors.NewGenericOAuthProvider(info, cfg, ssoSettings, features), nil
|
||||
return connectors.NewGenericOAuthProvider(info, cfg, orgRoleMapper, ssoSettings, features), nil
|
||||
case social.GitHubProviderName:
|
||||
return connectors.NewGitHubProvider(info, cfg, ssoSettings, features), nil
|
||||
return connectors.NewGitHubProvider(info, cfg, orgRoleMapper, ssoSettings, features), nil
|
||||
case social.GitlabProviderName:
|
||||
return connectors.NewGitLabProvider(info, cfg, ssoSettings, features), nil
|
||||
return connectors.NewGitLabProvider(info, cfg, orgRoleMapper, ssoSettings, features), nil
|
||||
case social.GoogleProviderName:
|
||||
return connectors.NewGoogleProvider(info, cfg, ssoSettings, features), nil
|
||||
return connectors.NewGoogleProvider(info, cfg, orgRoleMapper, ssoSettings, features), nil
|
||||
case social.GrafanaComProviderName:
|
||||
return connectors.NewGrafanaComProvider(info, cfg, ssoSettings, features), nil
|
||||
return connectors.NewGrafanaComProvider(info, cfg, orgRoleMapper, ssoSettings, features), nil
|
||||
case social.OktaProviderName:
|
||||
return connectors.NewOktaProvider(info, cfg, ssoSettings, features), nil
|
||||
return connectors.NewOktaProvider(info, cfg, orgRoleMapper, ssoSettings, features), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown oauth provider: %s", name)
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ func TestSocialService_ProvideService(t *testing.T) {
|
||||
tc.setup(t, env)
|
||||
}
|
||||
|
||||
socialService := ProvideService(cfg, env.features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeStore(t), ssoSettingsSvc)
|
||||
socialService := ProvideService(cfg, env.features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeStore(t), nil, ssoSettingsSvc)
|
||||
require.Equal(t, tc.expectedSocialMapLength, len(socialService.socialMap))
|
||||
|
||||
genericOAuthInfo := socialService.GetOAuthInfoProvider("generic_oauth")
|
||||
@ -192,7 +192,7 @@ func TestSocialService_ProvideService_GrafanaComGrafanaNet(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Raw = iniFile
|
||||
|
||||
socialService := ProvideService(cfg, featuremgmt.WithFeatures(), &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeStore(t), ssoSettingsSvc)
|
||||
socialService := ProvideService(cfg, featuremgmt.WithFeatures(), &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeStore(t), nil, ssoSettingsSvc)
|
||||
require.EqualValues(t, tc.expectedGrafanaComOAuthInfo, socialService.GetOAuthInfoProvider("grafana_com"))
|
||||
})
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats/validator"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/connectors"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialimpl"
|
||||
"github.com/grafana/grafana/pkg/middleware/csrf"
|
||||
"github.com/grafana/grafana/pkg/middleware/loggermw"
|
||||
@ -381,6 +382,7 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(auth.IDService), new(*idimpl.Service)),
|
||||
cloudmigrationimpl.ProvideService,
|
||||
userimpl.ProvideVerifier,
|
||||
connectors.ProvideOrgRoleMapper,
|
||||
wire.Bind(new(user.Verifier), new(*userimpl.Verifier)),
|
||||
// Kubernetes API server
|
||||
grafanaapiserver.WireSet,
|
||||
|
@ -166,9 +166,15 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
||||
return nil, errOAuthEmailNotAllowed.Errorf("provided email is not allowed")
|
||||
}
|
||||
|
||||
orgRoles, isGrafanaAdmin, _ := getRoles(c.cfg, func() (org.RoleType, *bool, error) {
|
||||
return userInfo.Role, userInfo.IsGrafanaAdmin, nil
|
||||
})
|
||||
// This is required to implement OrgRole mapping for OAuth providers step by step
|
||||
switch c.providerName {
|
||||
case social.GenericOAuthProviderName:
|
||||
// Do nothing, GenericOAuthProvider already supports OrgRole mapping
|
||||
default:
|
||||
userInfo.OrgRoles, userInfo.IsGrafanaAdmin, _ = getRoles(c.cfg, func() (org.RoleType, *bool, error) {
|
||||
return userInfo.Role, userInfo.IsGrafanaAdmin, nil
|
||||
})
|
||||
}
|
||||
|
||||
lookupParams := login.UserLookupParams{}
|
||||
allowInsecureEmailLookup := c.settingsProviderSvc.KeyValue("auth", "oauth_allow_insecure_email_lookup").MustBool(false)
|
||||
@ -180,12 +186,12 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
||||
Login: userInfo.Login,
|
||||
Name: userInfo.Name,
|
||||
Email: userInfo.Email,
|
||||
IsGrafanaAdmin: isGrafanaAdmin,
|
||||
IsGrafanaAdmin: userInfo.IsGrafanaAdmin,
|
||||
AuthenticatedBy: c.moduleName,
|
||||
AuthID: userInfo.Id,
|
||||
Groups: userInfo.Groups,
|
||||
OAuthToken: token,
|
||||
OrgRoles: orgRoles,
|
||||
OrgRoles: userInfo.OrgRoles,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncUser: true,
|
||||
SyncTeams: true,
|
||||
@ -193,7 +199,7 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
||||
SyncPermissions: true,
|
||||
AllowSignUp: connector.IsSignupAllowed(),
|
||||
// skip org role flag is checked and handled in the connector. For now we can skip the hook if no roles are passed
|
||||
SyncOrgRoles: len(orgRoles) > 0,
|
||||
SyncOrgRoles: len(userInfo.OrgRoles) > 0,
|
||||
LookUpParams: lookupParams,
|
||||
},
|
||||
}, nil
|
||||
|
@ -59,6 +59,11 @@ func (f *FakeOrgService) GetByID(ctx context.Context, query *org.GetOrgByIDQuery
|
||||
}
|
||||
|
||||
func (f *FakeOrgService) GetByName(ctx context.Context, query *org.GetOrgByNameQuery) (*org.Org, error) {
|
||||
for _, expectedOrg := range f.ExpectedOrgs {
|
||||
if expectedOrg != nil && expectedOrg.Name == query.Name {
|
||||
return &org.Org{ID: expectedOrg.ID, Name: expectedOrg.Name}, nil
|
||||
}
|
||||
}
|
||||
return f.ExpectedOrg, f.ExpectedError
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,8 @@ func (s *OAuthStrategy) loadSettingsForProvider(provider string) map[string]any
|
||||
"auto_login": section.Key("auto_login").MustBool(false),
|
||||
"allowed_groups": section.Key("allowed_groups").Value(),
|
||||
"signout_redirect_url": section.Key("signout_redirect_url").Value(),
|
||||
"org_mapping": section.Key("org_mapping").Value(),
|
||||
"org_attribute_path": section.Key("org_attribute_path").Value(),
|
||||
}
|
||||
|
||||
extraKeys := extraKeysByProvider[provider]
|
||||
|
@ -52,6 +52,8 @@ var (
|
||||
empty_scopes =
|
||||
hosted_domain = test_hosted_domain
|
||||
signout_redirect_url = test_signout_redirect_url
|
||||
org_attribute_path = groups
|
||||
org_mapping = Group1:*:Editor
|
||||
`
|
||||
|
||||
expectedOAuthInfo = map[string]any{
|
||||
@ -92,6 +94,8 @@ var (
|
||||
"login_attribute_path": "login",
|
||||
"name_attribute_path": "name",
|
||||
"team_ids": "first, second",
|
||||
"org_attribute_path": "groups",
|
||||
"org_mapping": "Group1:*:Editor",
|
||||
}
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user