mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Add org to role mappings support to AzureAD/Entra integration (#88861)
* Added implementation and tests * Add docs, simplify implementation * Remove unused func * Update docs
This commit is contained in:
parent
ee75fc3852
commit
9a44296bc2
@ -756,6 +756,7 @@ allowed_domains =
|
|||||||
allowed_groups =
|
allowed_groups =
|
||||||
allowed_organizations =
|
allowed_organizations =
|
||||||
role_attribute_strict = false
|
role_attribute_strict = false
|
||||||
|
org_mapping =
|
||||||
allow_assign_grafana_admin = false
|
allow_assign_grafana_admin = false
|
||||||
force_use_graph_api = false
|
force_use_graph_api = false
|
||||||
tls_skip_verify_insecure = false
|
tls_skip_verify_insecure = false
|
||||||
|
@ -694,6 +694,7 @@
|
|||||||
;allowed_groups =
|
;allowed_groups =
|
||||||
;allowed_organizations =
|
;allowed_organizations =
|
||||||
;role_attribute_strict = false
|
;role_attribute_strict = false
|
||||||
|
;org_mapping =
|
||||||
;allow_assign_grafana_admin = false
|
;allow_assign_grafana_admin = false
|
||||||
;use_pkce = true
|
;use_pkce = true
|
||||||
# prevent synchronizing users organization roles
|
# prevent synchronizing users organization roles
|
||||||
|
@ -411,12 +411,27 @@ By default, Azure AD authentication will map users to organization roles based o
|
|||||||
|
|
||||||
If no application role is found, the user is assigned the role specified by
|
If no application role is found, the user is assigned the role specified by
|
||||||
[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}).
|
[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`.
|
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 and the `org_mapping` expression evaluates to an empty mapping.
|
||||||
It denies user access if no role or an invalid role is returned.
|
|
||||||
|
You can use the `org_mapping` configuration option to assign the user to multiple organizations and specify their role based on their Entra ID group membership. For more information, refer to [Org roles mapping example](#org-roles-mapping-example). If the org role mapping (`org_mapping`) is specified and Entra ID returns a valid role, then the user will get the highest of the two roles.
|
||||||
|
|
||||||
**On every login** the user organization role will be reset to match Entra ID's application role and
|
**On every login** the user organization role will be reset to match Entra ID's application role and
|
||||||
their organization membership will be reset to the default organization.
|
their organization membership will be reset to the default organization.
|
||||||
|
|
||||||
|
#### Org roles mapping example
|
||||||
|
|
||||||
|
The Entra ID integration uses the external users' groups in the `org_mapping` configuration to map organizations and roles based on their Entra ID group membership.
|
||||||
|
|
||||||
|
In this example, the user has been granted the role of a `Viewer` in the `org_foo` organization, and the role of an `Editor` in the `org_bar` and `org_baz` orgs.
|
||||||
|
|
||||||
|
The external user is part of the following Entra ID groups: `032cb8e0-240f-4347-9120-6f33013e817a` and `bce1c492-0679-4989-941b-8de5e6789cb9`.
|
||||||
|
|
||||||
|
Config:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
org_mapping = ["032cb8e0-240f-4347-9120-6f33013e817a:org_foo:Viewer", "bce1c492-0679-4989-941b-8de5e6789cb9:org_bar:Editor", "*:org_baz:Editor"]
|
||||||
|
```
|
||||||
|
|
||||||
## Skip organization role sync
|
## Skip organization role sync
|
||||||
|
|
||||||
If Azure AD authentication is not intended to sync user roles and organization membership and prevent the sync of org roles from Entra ID, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana or that your organization roles are synced from another provider.
|
If Azure AD authentication is not intended to sync user roles and organization membership and prevent the sync of org roles from Entra ID, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana or that your organization roles are synced from another provider.
|
||||||
|
@ -148,6 +148,8 @@ The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.htm
|
|||||||
To map the server administrator role, use the `allow_assign_grafana_admin` configuration option.
|
To map the server administrator role, use the `allow_assign_grafana_admin` configuration option.
|
||||||
Refer to [configuration options]({{< relref "#configuration-options" >}}) for more information.
|
Refer to [configuration options]({{< relref "#configuration-options" >}}) for more information.
|
||||||
|
|
||||||
|
You can use the `org_mapping` configuration option to assign the user to multiple organizations and specify their role based on their GitLab group membership. For more information, refer to [Org roles mapping example](#org-roles-mapping-example). If the org role mapping (`org_mapping`) is specified and Entra ID returns a valid role, then the user will get the highest of the two roles.
|
||||||
|
|
||||||
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" >}}).
|
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 after evaluating the `role_attribute_path` and the `org_mapping` expressions.
|
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 after evaluating the `role_attribute_path` and the `org_mapping` expressions.
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||||
"github.com/grafana/grafana/pkg/login/social"
|
"github.com/grafana/grafana/pkg/login/social"
|
||||||
"github.com/grafana/grafana/pkg/models/roletype"
|
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
@ -121,26 +120,42 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
|
|||||||
return nil, ErrEmailNotFound
|
return nil, ErrEmailNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// setting the role, grafanaAdmin to empty to reflect that we are not syncronizing with the external provider
|
|
||||||
var role roletype.RoleType
|
|
||||||
var grafanaAdmin bool
|
|
||||||
if !s.info.SkipOrgRoleSync {
|
|
||||||
role, grafanaAdmin, err = s.extractRoleAndAdmin(claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !role.IsValid() {
|
|
||||||
return nil, errInvalidRole.Errorf("AzureAD OAuth: invalid role %q", role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.log.Debug("AzureAD OAuth: extracted role", "email", email, "role", role)
|
|
||||||
|
|
||||||
groups, err := s.extractGroups(ctx, client, claims, token)
|
groups, err := s.extractGroups(ctx, client, claims, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract groups: %w", err)
|
return nil, fmt.Errorf("failed to extract groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Debug("AzureAD OAuth: extracted groups", "email", email, "groups", fmt.Sprintf("%v", groups))
|
s.log.Debug("AzureAD OAuth: extracted groups", "email", email, "groups", fmt.Sprintf("%v", groups))
|
||||||
|
|
||||||
|
userInfo := &social.BasicUserInfo{
|
||||||
|
Id: claims.ID,
|
||||||
|
Name: claims.Name,
|
||||||
|
Email: email,
|
||||||
|
Login: email,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.info.SkipOrgRoleSync {
|
||||||
|
directlyMappedRole, grafanaAdmin := s.extractRoleAndAdminOptional(claims)
|
||||||
|
|
||||||
|
s.log.Debug("AzureAD OAuth: extracted role", "email", email, "role", directlyMappedRole)
|
||||||
|
|
||||||
|
if s.info.AllowAssignGrafanaAdmin {
|
||||||
|
userInfo.IsGrafanaAdmin = &grafanaAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, userInfo.Groups, directlyMappedRole)
|
||||||
|
if s.info.RoleAttributeStrict && len(userInfo.OrgRoles) == 0 {
|
||||||
|
return nil, errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Debug("AzureAD OAuth: mapped org roles", "email", email, "roles", fmt.Sprintf("%v", userInfo.OrgRoles))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync {
|
||||||
|
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
|
||||||
|
}
|
||||||
|
|
||||||
if !s.isGroupMember(groups) {
|
if !s.isGroupMember(groups) {
|
||||||
if len(groups) == 0 {
|
if len(groups) == 0 {
|
||||||
// either they do not have a group or misconfiguration
|
// either they do not have a group or misconfiguration
|
||||||
@ -150,24 +165,7 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
|
|||||||
return nil, errMissingGroupMembership
|
return nil, errMissingGroupMembership
|
||||||
}
|
}
|
||||||
|
|
||||||
var isGrafanaAdmin *bool = nil
|
return userInfo, nil
|
||||||
if s.info.AllowAssignGrafanaAdmin {
|
|
||||||
isGrafanaAdmin = &grafanaAdmin
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync {
|
|
||||||
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &social.BasicUserInfo{
|
|
||||||
Id: claims.ID,
|
|
||||||
Name: claims.Name,
|
|
||||||
Email: email,
|
|
||||||
Login: email,
|
|
||||||
Role: role,
|
|
||||||
IsGrafanaAdmin: isGrafanaAdmin,
|
|
||||||
Groups: groups,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
|
func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
|
||||||
@ -289,12 +287,9 @@ func (claims *azureClaims) extractEmail() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractRoleAndAdmin extracts the role from the claims and returns the role and whether the user is a Grafana admin.
|
// extractRoleAndAdmin extracts the role from the claims and returns the role and whether the user is a Grafana admin.
|
||||||
func (s *SocialAzureAD) extractRoleAndAdmin(claims *azureClaims) (org.RoleType, bool, error) {
|
func (s *SocialAzureAD) extractRoleAndAdminOptional(claims *azureClaims) (org.RoleType, bool) {
|
||||||
if len(claims.Roles) == 0 {
|
if len(claims.Roles) == 0 {
|
||||||
if s.info.RoleAttributeStrict {
|
return "", false
|
||||||
return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: unset role")
|
|
||||||
}
|
|
||||||
return s.defaultRole(), false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
roleOrder := []org.RoleType{social.RoleGrafanaAdmin, org.RoleAdmin, org.RoleEditor,
|
roleOrder := []org.RoleType{social.RoleGrafanaAdmin, org.RoleAdmin, org.RoleEditor,
|
||||||
@ -302,18 +297,14 @@ func (s *SocialAzureAD) extractRoleAndAdmin(claims *azureClaims) (org.RoleType,
|
|||||||
for _, role := range roleOrder {
|
for _, role := range roleOrder {
|
||||||
if found := hasRole(claims.Roles, role); found {
|
if found := hasRole(claims.Roles, role); found {
|
||||||
if role == social.RoleGrafanaAdmin {
|
if role == social.RoleGrafanaAdmin {
|
||||||
return org.RoleAdmin, true, nil
|
return org.RoleAdmin, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return role, false, nil
|
return role, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.info.RoleAttributeStrict {
|
return "", false
|
||||||
return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: idP did not return a valid role %q", claims.Roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.defaultRole(), false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasRole(roles []string, role org.RoleType) bool {
|
func hasRole(roles []string, role org.RoleType) bool {
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/login/social"
|
"github.com/grafana/grafana/pkg/login/social"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"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"
|
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||||
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
|
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
|
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
|
||||||
@ -75,12 +77,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -139,12 +141,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
usGovURL: true,
|
usGovURL: true,
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -166,12 +168,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -193,12 +195,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -220,12 +222,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -247,15 +249,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: @mgyongyosi check this test
|
|
||||||
{
|
{
|
||||||
name: "role from env variable",
|
name: "role from env variable",
|
||||||
claims: &azureClaims{
|
claims: &azureClaims{
|
||||||
@ -275,12 +276,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Editor",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -302,12 +303,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Editor",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -329,12 +330,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -362,7 +363,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
IsGrafanaAdmin: nil,
|
IsGrafanaAdmin: nil,
|
||||||
},
|
},
|
||||||
@ -391,7 +392,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Editor",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
IsGrafanaAdmin: falseBoolPtr(),
|
IsGrafanaAdmin: falseBoolPtr(),
|
||||||
},
|
},
|
||||||
@ -420,7 +421,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
IsGrafanaAdmin: trueBoolPtr(),
|
IsGrafanaAdmin: trueBoolPtr(),
|
||||||
},
|
},
|
||||||
@ -500,12 +501,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{"foo", "bar"},
|
Groups: []string{"foo", "bar"},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@ -531,12 +532,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{"foo"},
|
Groups: []string{"foo"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -585,12 +586,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
settingAutoAssignOrgRole: "",
|
settingAutoAssignOrgRole: "",
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1",
|
Id: "1",
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Login: "test@test.com",
|
Login: "test@test.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{"from_server"},
|
Groups: []string{"from_server"},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@ -620,12 +621,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
},
|
},
|
||||||
settingAutoAssignOrgRole: "",
|
settingAutoAssignOrgRole: "",
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1",
|
Id: "1",
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Email: "test@test.com",
|
Email: "test@test.com",
|
||||||
Login: "test@test.com",
|
Login: "test@test.com",
|
||||||
Role: "Viewer",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
Groups: []string{"from_server"},
|
Groups: []string{"from_server"},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@ -675,6 +676,131 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "should map role when org mapping is set, IdP returns with invalid role and role attribute strict is enabled",
|
||||||
|
fields: fields{
|
||||||
|
providerCfg: &social.OAuthInfo{
|
||||||
|
Name: "azuread",
|
||||||
|
ClientId: "client-id-example",
|
||||||
|
RoleAttributeStrict: true,
|
||||||
|
OrgMapping: []string{"group1:Org4:Editor", "*:5:Viewer"},
|
||||||
|
},
|
||||||
|
cfg: &setting.Cfg{},
|
||||||
|
},
|
||||||
|
claims: &azureClaims{
|
||||||
|
PreferredUsername: "",
|
||||||
|
Roles: []string{"Invalid"},
|
||||||
|
Groups: []string{"group1", "group3"},
|
||||||
|
Name: "My Name",
|
||||||
|
ID: "1234",
|
||||||
|
Email: "me@example.com",
|
||||||
|
},
|
||||||
|
want: &social.BasicUserInfo{
|
||||||
|
Id: "1234",
|
||||||
|
Name: "My Name",
|
||||||
|
Email: "me@example.com",
|
||||||
|
Login: "me@example.com",
|
||||||
|
OrgRoles: map[int64]org.RoleType{4: org.RoleEditor, 5: org.RoleViewer},
|
||||||
|
Groups: []string{"group1", "group3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should map role when org mapping is set and IdP returns with empty role list",
|
||||||
|
fields: fields{
|
||||||
|
providerCfg: &social.OAuthInfo{
|
||||||
|
Name: "azuread",
|
||||||
|
ClientId: "client-id-example",
|
||||||
|
OrgMapping: []string{"group1:Org4:Editor", "group2:5:Viewer"},
|
||||||
|
},
|
||||||
|
cfg: &setting.Cfg{
|
||||||
|
AutoAssignOrgRole: "Viewer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
claims: &azureClaims{
|
||||||
|
Email: "me@example.com",
|
||||||
|
PreferredUsername: "",
|
||||||
|
Roles: []string{},
|
||||||
|
Groups: []string{"group1"},
|
||||||
|
Name: "My Name",
|
||||||
|
ID: "1234",
|
||||||
|
},
|
||||||
|
want: &social.BasicUserInfo{
|
||||||
|
Id: "1234",
|
||||||
|
Name: "My Name",
|
||||||
|
Email: "me@example.com",
|
||||||
|
Login: "me@example.com",
|
||||||
|
OrgRoles: map[int64]org.RoleType{4: org.RoleEditor},
|
||||||
|
Groups: []string{"group1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should map role when only org mapping is set and role attribute strict is enabled",
|
||||||
|
fields: fields{
|
||||||
|
providerCfg: &social.OAuthInfo{
|
||||||
|
Name: "azuread",
|
||||||
|
ClientId: "client-id-example",
|
||||||
|
RoleAttributeStrict: true,
|
||||||
|
OrgMapping: []string{"group1:Org4:Editor", "*:5:Viewer"},
|
||||||
|
},
|
||||||
|
cfg: &setting.Cfg{},
|
||||||
|
},
|
||||||
|
claims: &azureClaims{
|
||||||
|
PreferredUsername: "",
|
||||||
|
Roles: []string{},
|
||||||
|
Groups: []string{"group1", "group3"},
|
||||||
|
Name: "My Name",
|
||||||
|
ID: "1234",
|
||||||
|
Email: "me@example.com",
|
||||||
|
},
|
||||||
|
want: &social.BasicUserInfo{
|
||||||
|
Id: "1234",
|
||||||
|
Name: "My Name",
|
||||||
|
Email: "me@example.com",
|
||||||
|
Login: "me@example.com",
|
||||||
|
OrgRoles: map[int64]org.RoleType{4: org.RoleEditor, 5: org.RoleViewer},
|
||||||
|
Groups: []string{"group1", "group3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return error when roles claim is empty and org mapping doesn't evaluate to any role and role attribute strict is enabled",
|
||||||
|
fields: fields{
|
||||||
|
providerCfg: &social.OAuthInfo{
|
||||||
|
Name: "azuread",
|
||||||
|
ClientId: "client-id-example",
|
||||||
|
RoleAttributeStrict: true,
|
||||||
|
OrgMapping: []string{"group1:Org4:Editor"},
|
||||||
|
},
|
||||||
|
cfg: &setting.Cfg{},
|
||||||
|
},
|
||||||
|
claims: &azureClaims{
|
||||||
|
PreferredUsername: "",
|
||||||
|
Roles: []string{},
|
||||||
|
Groups: []string{"group2"},
|
||||||
|
Name: "My Name",
|
||||||
|
ID: "1234",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return error when roles claim is empty and org mapping is empty and role attribute strict is enabled",
|
||||||
|
fields: fields{
|
||||||
|
providerCfg: &social.OAuthInfo{
|
||||||
|
Name: "azuread",
|
||||||
|
ClientId: "client-id-example",
|
||||||
|
RoleAttributeStrict: true,
|
||||||
|
OrgMapping: []string{},
|
||||||
|
},
|
||||||
|
cfg: &setting.Cfg{},
|
||||||
|
},
|
||||||
|
claims: &azureClaims{
|
||||||
|
PreferredUsername: "",
|
||||||
|
Roles: []string{},
|
||||||
|
Groups: []string{"group2"},
|
||||||
|
Name: "My Name",
|
||||||
|
ID: "1234",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
@ -711,7 +837,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
|
s := NewAzureADProvider(tt.fields.providerCfg,
|
||||||
|
tt.fields.cfg,
|
||||||
|
ProvideOrgRoleMapper(tt.fields.cfg,
|
||||||
|
&orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}),
|
||||||
|
&ssosettingstests.MockService{},
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
cache)
|
||||||
|
|
||||||
if tt.fields.usGovURL {
|
if tt.fields.usGovURL {
|
||||||
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
||||||
@ -765,11 +897,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
|||||||
tt.args.client = s.Client(context.Background(), token)
|
tt.args.client = s.Client(context.Background(), token)
|
||||||
|
|
||||||
got, err := s.UserInfo(context.Background(), tt.args.client, token)
|
got, err := s.UserInfo(context.Background(), tt.args.client, token)
|
||||||
if (err != nil) != tt.wantErr {
|
if tt.wantErr {
|
||||||
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, tt.want, got)
|
require.EqualValues(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -791,7 +924,7 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
|||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled",
|
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
providerCfg: &social.OAuthInfo{
|
providerCfg: &social.OAuthInfo{
|
||||||
Name: "azuread",
|
Name: "azuread",
|
||||||
@ -815,19 +948,19 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
|||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
IsGrafanaAdmin: trueBoolPtr(),
|
IsGrafanaAdmin: trueBoolPtr(),
|
||||||
Groups: []string{},
|
Groups: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles",
|
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync enabled should not get roles",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
providerCfg: &social.OAuthInfo{
|
providerCfg: &social.OAuthInfo{
|
||||||
Name: "azuread",
|
Name: "azuread",
|
||||||
ClientId: "client-id-example",
|
ClientId: "client-id-example",
|
||||||
AllowAssignGrafanaAdmin: true,
|
AllowAssignGrafanaAdmin: true,
|
||||||
SkipOrgRoleSync: false,
|
SkipOrgRoleSync: true,
|
||||||
},
|
},
|
||||||
cfg: &setting.Cfg{
|
cfg: &setting.Cfg{
|
||||||
AutoAssignOrgRole: "",
|
AutoAssignOrgRole: "",
|
||||||
@ -841,13 +974,11 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
|||||||
ID: "1234",
|
ID: "1234",
|
||||||
},
|
},
|
||||||
want: &social.BasicUserInfo{
|
want: &social.BasicUserInfo{
|
||||||
Id: "1234",
|
Id: "1234",
|
||||||
Name: "My Name",
|
Name: "My Name",
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Login: "me@example.com",
|
Login: "me@example.com",
|
||||||
Role: "Admin",
|
Groups: []string{},
|
||||||
IsGrafanaAdmin: trueBoolPtr(),
|
|
||||||
Groups: []string{},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -884,7 +1015,13 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, nil, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
|
s := NewAzureADProvider(tt.fields.providerCfg,
|
||||||
|
tt.fields.cfg,
|
||||||
|
ProvideOrgRoleMapper(tt.fields.cfg,
|
||||||
|
&orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}),
|
||||||
|
&ssosettingstests.MockService{},
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
cache)
|
||||||
|
|
||||||
s.SocialBase.Endpoint.AuthURL = authURL
|
s.SocialBase.Endpoint.AuthURL = authURL
|
||||||
|
|
||||||
|
@ -177,18 +177,6 @@ func (s *SocialBase) extractOrgs(rawJSON []byte) ([]string, error) {
|
|||||||
return util.SearchJSONForStringSliceAttr(s.info.OrgAttributePath, rawJSON)
|
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 {
|
|
||||||
if s.cfg.AutoAssignOrgRole != "" {
|
|
||||||
s.log.Debug("No role found, returning default.")
|
|
||||||
return org.RoleType(s.cfg.AutoAssignOrgRole)
|
|
||||||
}
|
|
||||||
|
|
||||||
// should never happen
|
|
||||||
return org.RoleViewer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialBase) isGroupMember(groups []string) bool {
|
func (s *SocialBase) isGroupMember(groups []string) bool {
|
||||||
if len(s.info.AllowedGroups) == 0 {
|
if len(s.info.AllowedGroups) == 0 {
|
||||||
return true
|
return true
|
||||||
|
@ -168,8 +168,8 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
|||||||
|
|
||||||
// This is required to implement OrgRole mapping for OAuth providers step by step
|
// This is required to implement OrgRole mapping for OAuth providers step by step
|
||||||
switch c.providerName {
|
switch c.providerName {
|
||||||
case social.GenericOAuthProviderName, social.GitHubProviderName,
|
case social.GenericOAuthProviderName, social.GitHubProviderName, social.GitlabProviderName,
|
||||||
social.GitlabProviderName, social.OktaProviderName, social.GoogleProviderName:
|
social.OktaProviderName, social.GoogleProviderName, social.AzureADProviderName:
|
||||||
// Do nothing, these providers already supports OrgRole mapping
|
// Do nothing, these providers already supports OrgRole mapping
|
||||||
default:
|
default:
|
||||||
userInfo.OrgRoles, userInfo.IsGrafanaAdmin, _ = getRoles(c.cfg, func() (org.RoleType, *bool, error) {
|
userInfo.OrgRoles, userInfo.IsGrafanaAdmin, _ = getRoles(c.cfg, func() (org.RoleType, *bool, error) {
|
||||||
|
Loading…
Reference in New Issue
Block a user