Backport: Revert changes to oauth behavior to v9.1.x (#55302)

* OAuth: Allow assigning Server Admin (#54780)

* extract errors to errors file

* implement oauth server admin assignment

* add server admin tests

* deduplicate autoAssignOrgRole

* deduplicate strict setting

* deduplicate strict setting

* add support for generic oauth

* add role attribute strict support for generic oauth

* add support for github/gitlab

* assignGrafanaAdmin option is here to stay

* unify similar errors

* add config option

* add okta server admin mapping

* remove never used Company attribute

* unify generic oauth role extract with other methods

* case insensitive role match as in azure

* add ini settings

* add server admin to devenv

* remove duplicate fields

* add documentation to oauth

* fix titlecase test

* implement doc feedback

(cherry picked from commit ef245874da)

* Auth: Restore legacy behavior and add deprecation notice for empty org role in oauth (#55118)

* Auth: Add deprecation notice for empty org role

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* fix recasts

* fix azure tests missing logger

* Adding test to gitlab oauth

* Covering more cases

* Cover more options

* Add role attributestrict check fail

* Adding one more edge case test

* Using legacy for gitlab

* Yet another edge case YAEC

* Reverting github oauth to legacy

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Not using token

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Nit.

* Adding warning in docs

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* add warning to generic oauth

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Be more precise

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Adding warning to github oauth

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Adding warning to gitlab oauth

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Adding warning to okta oauth

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Add docs about mapping to AzureAD

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Clarify oauth_skip_org_role_update_sync

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Nit.

* Nit on Azure AD

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Reorder docs index

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Fix typo

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: gamab <gabi.mabs@gmail.com>
(cherry picked from commit 00e7324bf6)

* Auth: Allow admins to manually change oauth user role if `oauth_skip_org_role_update_sync` is enabled  (#55182)

* Auth: Allow admins to change oauth user info it it's not synced.

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Update public/app/features/admin/UserAdminPage.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Add missing import

* Simplify init

Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* SAML: Add option to skip org role sync (#55230)

* SAML: Add option to skip org role sync

* Modify frontend accordingly

* Remove update from config option name

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Remove update from config option name

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Fix typo

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: gamab <gabi.mabs@gmail.com>
Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
(cherry picked from commit 3e2e9f93b9)

* Update gitlab_oauth_test.go

* Update gitlab_oauth_test.go
This commit is contained in:
Jo
2022-09-19 09:33:47 +02:00
committed by GitHub
parent 93db981c89
commit 5f51a1425b
30 changed files with 886 additions and 370 deletions
+4
View File
@@ -135,6 +135,10 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"editorsCanAdmin": hs.Cfg.EditorsCanAdmin,
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
"pluginsToPreload": pluginsToPreload,
"auth": map[string]interface{}{
"OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync,
"SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false),
},
"buildInfo": map[string]interface{}{
"hideVersion": hideVersion,
"version": version,
+10 -9
View File
@@ -262,18 +262,19 @@ func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *socia
oauthLogger.Debug("Building external user info from OAuth user info")
extUser := &models.ExternalUserInfo{
AuthModule: fmt.Sprintf("oauth_%s", name),
OAuthToken: token,
AuthId: userInfo.Id,
Name: userInfo.Name,
Login: userInfo.Login,
Email: userInfo.Email,
OrgRoles: map[int64]models.RoleType{},
Groups: userInfo.Groups,
AuthModule: fmt.Sprintf("oauth_%s", name),
OAuthToken: token,
AuthId: userInfo.Id,
Name: userInfo.Name,
Login: userInfo.Login,
Email: userInfo.Email,
OrgRoles: map[int64]models.RoleType{},
Groups: userInfo.Groups,
IsGrafanaAdmin: userInfo.IsGrafanaAdmin,
}
if userInfo.Role != "" && !hs.Cfg.OAuthSkipOrgRoleUpdateSync {
rt := models.RoleType(userInfo.Role)
rt := userInfo.Role
if rt.IsValid() {
// The user will be assigned a role in either the auto-assigned organization or in the default one
var orgID int64
+30 -33
View File
@@ -3,7 +3,6 @@ package social
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -17,9 +16,7 @@ import (
type SocialAzureAD struct {
*SocialBase
allowedGroups []string
autoAssignOrgRole string
roleAttributeStrict bool
allowedGroups []string
}
type azureClaims struct {
@@ -52,7 +49,7 @@ func (s *SocialAzureAD) Type() int {
func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
idToken := token.Extra("id_token")
if idToken == nil {
return nil, fmt.Errorf("no id_token found")
return nil, ErrIDTokenNotFound
}
parsedToken, err := jwt.ParseSigned(idToken.(string))
@@ -67,13 +64,14 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas
email := claims.extractEmail()
if email == "" {
return nil, errors.New("error getting user info: no email found in access token")
return nil, ErrEmailNotFound
}
role := claims.extractRole(s.autoAssignOrgRole, s.roleAttributeStrict)
if role == "" {
return nil, errors.New("user does not have a valid role")
role, grafanaAdmin := s.extractRoleAndAdmin(&claims)
if s.roleAttributeStrict && !role.IsValid() {
return nil, ErrInvalidBasicRole
}
logger.Debug("AzureAD OAuth: extracted role", "email", email, "role", role)
groups, err := extractGroups(client, claims, token)
@@ -86,13 +84,19 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas
return nil, errMissingGroupMembership
}
var isGrafanaAdmin *bool = nil
if s.allowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
}
return &BasicUserInfo{
Id: claims.ID,
Name: claims.Name,
Email: email,
Login: email,
Role: string(role),
Groups: groups,
Id: claims.ID,
Name: claims.Name,
Email: email,
Login: email,
Role: role,
IsGrafanaAdmin: isGrafanaAdmin,
Groups: groups,
}, nil
}
@@ -122,32 +126,24 @@ func (claims *azureClaims) extractEmail() string {
return claims.Email
}
func (claims *azureClaims) extractRole(autoAssignRole string, strictMode bool) models.RoleType {
// 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) (models.RoleType, bool) {
if len(claims.Roles) == 0 {
if strictMode {
return models.RoleType("")
}
return models.RoleType(autoAssignRole)
}
roleOrder := []models.RoleType{
models.ROLE_ADMIN,
models.ROLE_EDITOR,
models.ROLE_VIEWER,
return s.defaultRole(false), false
}
roleOrder := []models.RoleType{RoleGrafanaAdmin, models.ROLE_ADMIN, models.ROLE_EDITOR, models.ROLE_VIEWER}
for _, role := range roleOrder {
if found := hasRole(claims.Roles, role); found {
return role
if role == RoleGrafanaAdmin {
return models.ROLE_ADMIN, true
}
return role, false
}
}
if strictMode {
return models.RoleType("")
}
return models.ROLE_VIEWER
return s.defaultRole(false), false
}
func hasRole(roles []string, role models.RoleType) bool {
@@ -156,6 +152,7 @@ func hasRole(roles []string, role models.RoleType) bool {
return true
}
}
return false
}
+151 -88
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
@@ -16,12 +15,20 @@ import (
"gopkg.in/square/go-jose.v2/jwt"
)
func trueBoolPtr() *bool {
b := true
return &b
}
func falseBoolPtr() *bool {
b := false
return &b
}
func TestSocialAzureAD_UserInfo(t *testing.T) {
type fields struct {
SocialBase *SocialBase
allowedGroups []string
autoAssignOrgRole string
roleAttributeStrict bool
SocialBase *SocialBase
allowedGroups []string
}
type args struct {
client *http.Client
@@ -46,16 +53,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
autoAssignOrgRole: "Viewer",
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer"),
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Viewer",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Viewer",
Groups: []string{},
},
},
{
@@ -86,16 +92,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
autoAssignOrgRole: "Viewer",
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer"),
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Viewer",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Viewer",
Groups: []string{},
},
},
{
@@ -108,13 +113,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Admin",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Admin",
Groups: []string{},
},
},
{
@@ -127,17 +131,19 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Admin",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Admin",
Groups: []string{},
},
},
{
name: "Only other roles",
fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer"),
},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
@@ -146,13 +152,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Viewer",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Viewer",
Groups: []string{},
},
},
{
@@ -165,16 +170,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
autoAssignOrgRole: "Editor",
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor"),
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Editor",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Editor",
Groups: []string{},
},
},
{
@@ -187,13 +191,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Editor",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Editor",
Groups: []string{},
},
},
{
@@ -206,13 +209,75 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Admin",
Groups: []string{},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Admin",
Groups: []string{},
},
},
{
name: "Grafana Admin but setting is disabled",
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor")},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
Roles: []string{"GrafanaAdmin"},
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Admin",
Groups: []string{},
IsGrafanaAdmin: nil,
},
},
{
name: "Editor roles in claim and GrafanaAdminAssignment enabled",
fields: fields{
SocialBase: newSocialBase("azuread",
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "")},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
Roles: []string{"Editor"},
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Editor",
Groups: []string{},
IsGrafanaAdmin: falseBoolPtr(),
},
},
{
name: "Grafana Admin and Editor roles in claim",
fields: fields{SocialBase: newSocialBase("azuread",
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "")},
claims: &azureClaims{
Email: "me@example.com",
PreferredUsername: "",
Roles: []string{"GrafanaAdmin", "Editor"},
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Admin",
Groups: []string{},
IsGrafanaAdmin: trueBoolPtr(),
},
},
{
@@ -234,8 +299,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Error if user is a member of allowed_groups",
fields: fields{
allowedGroups: []string{"foo", "bar"},
autoAssignOrgRole: "Viewer",
allowedGroups: []string{"foo", "bar"},
SocialBase: newSocialBase("azuread",
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer"),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -246,13 +312,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
want: &BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Company: "",
Role: "Viewer",
Groups: []string{"foo"},
Id: "1234",
Name: "My Name",
Email: "me@example.com",
Login: "me@example.com",
Role: "Viewer",
Groups: []string{"foo"},
},
},
{
@@ -283,7 +348,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch empty role when strict attribute role is true and no match",
fields: fields{
roleAttributeStrict: true,
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, ""),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -299,7 +364,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch empty role when strict attribute role is true and no role claims returned",
fields: fields{
roleAttributeStrict: true,
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, ""),
},
claims: &azureClaims{
Email: "me@example.com",
@@ -313,13 +378,16 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SocialAzureAD{
SocialBase: tt.fields.SocialBase,
allowedGroups: tt.fields.allowedGroups,
autoAssignOrgRole: tt.fields.autoAssignOrgRole,
roleAttributeStrict: tt.fields.roleAttributeStrict,
SocialBase: tt.fields.SocialBase,
allowedGroups: tt.fields.allowedGroups,
}
if tt.fields.SocialBase == nil {
s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "")
}
key := []byte("secret")
@@ -357,14 +425,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
}
}
raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize()
if err != nil {
t.Error(err)
}
require.NoError(t, err)
} else {
raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize()
if err != nil {
t.Error(err)
}
require.NoError(t, err)
}
token := &oauth2.Token{
@@ -383,9 +447,8 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UserInfo() got = %v, want %v", got, tt.want)
}
require.EqualValues(t, tt.want, got)
})
}
}
+9
View File
@@ -0,0 +1,9 @@
package social
import "errors"
var (
ErrIDTokenNotFound = errors.New("id_token not found")
ErrInvalidBasicRole = errors.New("user does not have a valid basic role")
ErrEmailNotFound = errors.New("error getting user info: no email found in access token")
)
+10 -23
View File
@@ -26,8 +26,6 @@ type SocialGenericOAuth struct {
emailAttributePath string
loginAttributePath string
nameAttributePath string
roleAttributePath string
roleAttributeStrict bool
groupsAttributePath string
idTokenAttributeName string
teamIdsAttributePath string
@@ -146,12 +144,14 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
}
if userInfo.Role == "" {
role, err := s.extractRole(data)
if err != nil {
s.log.Warn("Failed to extract role", "error", err)
} else if role != "" {
role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, []string{}, true)
if role != "" {
s.log.Debug("Setting user info role from extracted role")
userInfo.Role = role
if s.allowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin
}
}
}
@@ -166,6 +166,10 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
}
}
if s.roleAttributeStrict && !userInfo.Role.IsValid() {
return nil, ErrInvalidBasicRole
}
if userInfo.Email == "" {
var err error
userInfo.Email, err = s.FetchPrivateEmail(client)
@@ -180,10 +184,6 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
userInfo.Login = userInfo.Email
}
if s.roleAttributeStrict && !models.RoleType(userInfo.Role).IsValid() {
return nil, errors.New("invalid role")
}
if !s.IsTeamMember(client) {
return nil, errors.New("user not a member of one of the required teams")
}
@@ -354,19 +354,6 @@ func (s *SocialGenericOAuth) extractUserName(data *UserInfoJson) string {
return ""
}
func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) (string, error) {
if s.roleAttributePath == "" {
return "", nil
}
role, err := s.searchJSONForStringAttr(s.roleAttributePath, data.rawJSON)
if err != nil {
return "", err
}
return role, nil
}
func (s *SocialGenericOAuth) extractGroups(data *UserInfoJson) ([]string, error) {
if s.groupsAttributePath == "" {
return []string{}, nil
+45 -7
View File
@@ -13,6 +13,7 @@ import (
"github.com/go-kit/log/level"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
func newLogger(name string, lev string) log.Logger {
@@ -245,12 +246,14 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
}
tests := []struct {
Name string
ResponseBody interface{}
OAuth2Extra interface{}
RoleAttributePath string
ExpectedEmail string
ExpectedRole string
Name string
AllowAssignGrafanaAdmin bool
ResponseBody interface{}
OAuth2Extra interface{}
RoleAttributePath string
ExpectedEmail string
ExpectedRole models.RoleType
ExpectedGrafanaAdmin *bool
}{
{
Name: "Given a valid id_token, a valid role path, no API response, use id_token",
@@ -330,6 +333,38 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
},
{
Name: "Given a valid id_token and AssignGrafanaAdmin is unchecked, don't grant Server Admin",
AllowAssignGrafanaAdmin: false,
OAuth2Extra: map[string]interface{}{
// { "role": "GrafanaAdmin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI",
},
ResponseBody: map[string]interface{}{
"role": "FromResponse",
"email": "from_response@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: nil,
},
{
Name: "Given a valid id_token and AssignGrafanaAdmin is checked, grant Server Admin",
AllowAssignGrafanaAdmin: true,
OAuth2Extra: map[string]interface{}{
// { "role": "GrafanaAdmin", "email": "john.doe@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSJ9.cQqMJpVjwdtJ8qEZLOo9RKNbAFfpkQcpnRG0nopmWEI",
},
ResponseBody: map[string]interface{}{
"role": "FromResponse",
"email": "from_response@example.com",
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: trueBoolPtr(),
},
{
Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token",
OAuth2Extra: map[string]interface{}{
@@ -368,7 +403,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
},
RoleAttributePath: "role",
ExpectedEmail: "john.doe@example.com",
ExpectedRole: "FromResponse",
ExpectedRole: "Fromresponse",
},
{
Name: "Given a valid id_token, a valid advanced JMESPath role path, derive the role",
@@ -416,6 +451,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath
provider.allowAssignGrafanaAdmin = test.AllowAssignGrafanaAdmin
t.Run(test.Name, func(t *testing.T) {
body, err := json.Marshal(test.ResponseBody)
require.NoError(t, err)
@@ -439,6 +476,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
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)
})
}
})
+14 -11
View File
@@ -201,21 +201,24 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
teams := convertToGroupList(teamMemberships)
role, err := s.extractRole(response.Body, teams)
if err != nil {
s.log.Error("Failed to extract role", "error", err)
}
role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, teams, true)
if s.roleAttributeStrict && !role.IsValid() {
return nil, errors.New("invalid role")
return nil, ErrInvalidBasicRole
}
var isGrafanaAdmin *bool = nil
if s.allowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
}
userInfo := &BasicUserInfo{
Name: data.Login,
Login: data.Login,
Id: fmt.Sprintf("%d", data.Id),
Email: data.Email,
Role: string(role),
Groups: teams,
Name: data.Login,
Login: data.Login,
Id: fmt.Sprintf("%d", data.Id),
Email: data.Email,
Role: role,
Groups: teams,
IsGrafanaAdmin: isGrafanaAdmin,
}
if data.Name != "" {
userInfo.Name = data.Name
+26 -30
View File
@@ -127,13 +127,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
autoAssignOrgRole: "",
roleAttributePath: "",
want: &BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Company: "",
Role: "",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
@@ -143,13 +142,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Company: "",
Role: "Admin",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Admin",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
@@ -159,29 +157,27 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Company: "",
Role: "Editor",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Editor",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "auto assign org role",
{ // Case that's going to change with Grafana 10
name: "No fallback to default org role (will change in Grafana 10)",
roleAttributePath: "",
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Company: "",
Role: "Editor",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
}
+15 -13
View File
@@ -2,7 +2,6 @@ package social
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
@@ -90,7 +89,7 @@ func (s *SocialGitlab) GetGroupsPage(client *http.Client, url string) ([]string,
return fullPaths, next
}
func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGitlab) UserInfo(client *http.Client, _ *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id int
Username string
@@ -114,21 +113,24 @@ func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
groups := s.GetGroups(client)
role, err := s.extractRole(response.Body, groups)
if err != nil {
s.log.Error("Failed to extract role", "error", err)
}
role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, groups, true)
if s.roleAttributeStrict && !role.IsValid() {
return nil, errors.New("invalid role")
return nil, ErrInvalidBasicRole
}
var isGrafanaAdmin *bool = nil
if s.allowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
}
userInfo := &BasicUserInfo{
Id: fmt.Sprintf("%d", data.Id),
Name: data.Name,
Login: data.Username,
Email: data.Email,
Groups: groups,
Role: string(role),
Id: fmt.Sprintf("%d", data.Id),
Name: data.Name,
Login: data.Username,
Email: data.Email,
Groups: groups,
Role: role,
IsGrafanaAdmin: isGrafanaAdmin,
}
if !s.IsGroupMember(groups) {
+159
View File
@@ -0,0 +1,159 @@
package social
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
const (
apiURI = "/api/v4"
userURI = "/api/v4/user"
groupsURI = "/api/v4/groups"
gitlabAttrPath = `is_admin && 'GrafanaAdmin' || contains(groups[*], 'admins') && 'Admin' || contains(groups[*], 'editors') && 'Editor' || contains(groups[*], 'viewers') && 'Viewer'`
rootUserRespBody = `{"id":1,"username":"root","name":"Administrator","state":"active","email":"root@example.org","is_admin":true,"namespace_id":1}`
editorUserRespBody = `{"id":3,"username":"gitlab-editor","name":"Gitlab Editor","state":"active","email":"gitlab-editor@example.org","is_admin":false,"namespace_id":1}`
adminGroup = `{"id":4,"web_url":"http://grafana-gitlab.local/groups/admins","name":"Admins","path":"admins","project_creation_level":"developer","full_name":"Admins","full_path":"admins","created_at":"2022-09-13T19:38:04.891Z"}`
editorGroup = `{"id":5,"web_url":"http://grafana-gitlab.local/groups/editors","name":"Editors","path":"editors","project_creation_level":"developer","full_name":"Editors","full_path":"editors","created_at":"2022-09-13T19:38:15.074Z"}`
viewerGroup = `{"id":6,"web_url":"http://grafana-gitlab.local/groups/viewers","name":"Viewers","path":"viewers","project_creation_level":"developer","full_name":"Viewers","full_path":"viewers","created_at":"2022-09-13T19:38:25.777Z"}`
// serverAdminGroup = `{"id":7,"web_url":"http://grafana-gitlab.local/groups/serveradmins","name":"ServerAdmins","path":"serveradmins","project_creation_level":"developer","full_name":"ServerAdmins","full_path":"serveradmins","created_at":"2022-09-13T19:38:36.227Z"}`
)
func TestSocialGitlab_UserInfo(t *testing.T) {
provider := SocialGitlab{
SocialBase: &SocialBase{
log: newLogger("gitlab_oauth_test", "debug"),
},
}
type conf struct {
AllowAssignGrafanaAdmin bool
RoleAttributeStrict bool
AutoAssignOrgRole models.RoleType
}
tests := []struct {
Name string
Cfg conf
UserRespBody string
GroupsRespBody string
RoleAttributePath string
ExpectedLogin string
ExpectedEmail string
ExpectedRole models.RoleType
ExpectedGrafanaAdmin *bool
ExpectedError error
}{
{
Name: "Server Admin Allowed",
Cfg: conf{AllowAssignGrafanaAdmin: true},
UserRespBody: rootUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{adminGroup, editorGroup, viewerGroup}, ",") + "]",
RoleAttributePath: gitlabAttrPath,
ExpectedLogin: "root",
ExpectedEmail: "root@example.org",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: trueBoolPtr(),
},
{ // Edge case, user in Viewer Group, Server Admin disabled but attribute path contains a condition for Server Admin => User has the Admin role
Name: "Server Admin Disabled",
Cfg: conf{AllowAssignGrafanaAdmin: false},
UserRespBody: rootUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{viewerGroup}, ",") + "]",
RoleAttributePath: gitlabAttrPath,
ExpectedLogin: "root",
ExpectedEmail: "root@example.org",
ExpectedRole: "Admin",
},
{
Name: "Editor",
Cfg: conf{AllowAssignGrafanaAdmin: true},
UserRespBody: editorUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{viewerGroup, editorGroup}, ",") + "]",
RoleAttributePath: gitlabAttrPath,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRole: "Editor",
ExpectedGrafanaAdmin: falseBoolPtr(),
},
{ // Case that's going to change with Grafana 10
Name: "No fallback to default org role (will change in Grafana 10)",
Cfg: conf{AutoAssignOrgRole: models.ROLE_VIEWER},
UserRespBody: editorUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{}, ",") + "]",
RoleAttributePath: gitlabAttrPath,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRole: "",
},
{
Name: "Strict mode prevents fallback to default",
Cfg: conf{RoleAttributeStrict: true, AutoAssignOrgRole: models.ROLE_VIEWER},
UserRespBody: editorUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{}, ",") + "]",
RoleAttributePath: gitlabAttrPath,
ExpectedError: ErrInvalidBasicRole,
},
{ // Edge case, no match, no strict mode and no fallback => User has an empty role
Name: "Fallback with no default will create a user with an empty role",
Cfg: conf{},
UserRespBody: editorUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{}, ",") + "]",
RoleAttributePath: gitlabAttrPath,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRole: "",
},
{ // Edge case, no attribute path with strict mode => User has an empty role
Name: "Strict mode with no attribute path",
Cfg: conf{RoleAttributeStrict: true, AutoAssignOrgRole: models.ROLE_VIEWER},
UserRespBody: editorUserRespBody,
GroupsRespBody: "[" + strings.Join([]string{editorGroup}, ",") + "]",
RoleAttributePath: "",
ExpectedError: ErrInvalidBasicRole,
},
}
for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath
provider.allowAssignGrafanaAdmin = test.Cfg.AllowAssignGrafanaAdmin
provider.autoAssignOrgRole = string(test.Cfg.AutoAssignOrgRole)
provider.roleAttributeStrict = test.Cfg.RoleAttributeStrict
t.Run(test.Name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
switch r.RequestURI {
case userURI:
_, err := w.Write([]byte(test.UserRespBody))
require.NoError(t, err)
case groupsURI:
_, err := w.Write([]byte(test.GroupsRespBody))
require.NoError(t, err)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
provider.apiUrl = ts.URL + apiURI
actualResult, err := provider.UserInfo(ts.Client(), nil)
if test.ExpectedError != nil {
require.Equal(t, err, test.ExpectedError)
return
}
require.NoError(t, err)
require.Equal(t, test.ExpectedEmail, actualResult.Email)
require.Equal(t, test.ExpectedLogin, actualResult.Login)
require.Equal(t, test.ExpectedRole, actualResult.Role)
require.Equal(t, test.ExpectedGrafanaAdmin, actualResult.IsGrafanaAdmin)
})
}
}
+2 -2
View File
@@ -44,7 +44,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
return false
}
func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGrafanaCom) UserInfo(client *http.Client, _ *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
Name string `json:"name"`
@@ -69,7 +69,7 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*
Name: data.Name,
Login: data.Login,
Email: data.Email,
Role: data.Role,
Role: models.RoleType(data.Role),
}
if !s.IsOrganizationMember(data.Orgs) {
+19 -30
View File
@@ -13,10 +13,8 @@ import (
type SocialOkta struct {
*SocialBase
apiUrl string
allowedGroups []string
roleAttributePath string
roleAttributeStrict bool
apiUrl string
allowedGroups []string
}
type OktaUserInfoJson struct {
@@ -77,26 +75,29 @@ func (s *SocialOkta) UserInfo(client *http.Client, token *oauth2.Token) (*BasicU
return nil, err
}
role, err := s.extractRole(&data)
if err != nil {
s.log.Error("Failed to extract role", "error", err)
}
if s.roleAttributeStrict && !models.RoleType(role).IsValid() {
return nil, errors.New("invalid role")
}
groups := s.GetGroups(&data)
if !s.IsGroupMember(groups) {
return nil, errMissingGroupMembership
}
role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, groups, true)
if s.roleAttributeStrict && !role.IsValid() {
return nil, ErrInvalidBasicRole
}
var isGrafanaAdmin *bool = nil
if s.allowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
}
return &BasicUserInfo{
Id: claims.ID,
Name: claims.Name,
Email: email,
Login: email,
Role: role,
Groups: groups,
Id: claims.ID,
Name: claims.Name,
Email: email,
Login: email,
Role: role,
IsGrafanaAdmin: isGrafanaAdmin,
Groups: groups,
}, nil
}
@@ -119,18 +120,6 @@ func (s *SocialOkta) extractAPI(data *OktaUserInfoJson, client *http.Client) err
return nil
}
func (s *SocialOkta) extractRole(data *OktaUserInfoJson) (string, error) {
if s.roleAttributePath == "" {
return "", nil
}
role, err := s.searchJSONForStringAttr(s.roleAttributePath, data.rawJSON)
if err != nil {
return "", err
}
return role, nil
}
func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string {
groups := make([]string, 0)
if len(data.Groups) > 0 {
+117 -88
View File
@@ -12,6 +12,8 @@ import (
"context"
"golang.org/x/oauth2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@@ -31,28 +33,29 @@ type SocialService struct {
}
type OAuthInfo struct {
ClientId, ClientSecret string
Scopes []string
AuthUrl, TokenUrl string
Enabled bool
EmailAttributeName string
EmailAttributePath string
RoleAttributePath string
RoleAttributeStrict bool
GroupsAttributePath string
TeamIdsAttributePath string
AllowedDomains []string
HostedDomain string
ApiUrl string
TeamsUrl string
AllowSignup bool
Name string
Icon string
TlsClientCert string
TlsClientKey string
TlsClientCa string
TlsSkipVerify bool
UsePKCE bool
ClientId, ClientSecret string
Scopes []string
AuthUrl, TokenUrl string
Enabled bool
EmailAttributeName string
EmailAttributePath string
RoleAttributePath string
RoleAttributeStrict bool
GroupsAttributePath string
TeamIdsAttributePath string
AllowedDomains []string
AllowAssignGrafanaAdmin bool
HostedDomain string
ApiUrl string
TeamsUrl string
AllowSignup bool
Name string
Icon string
TlsClientCert string
TlsClientKey string
TlsClientCa string
TlsSkipVerify bool
UsePKCE bool
}
func ProvideService(cfg *setting.Cfg) *SocialService {
@@ -66,30 +69,31 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
sec := cfg.Raw.Section("auth." + name)
info := &OAuthInfo{
ClientId: sec.Key("client_id").String(),
ClientSecret: sec.Key("client_secret").String(),
Scopes: util.SplitString(sec.Key("scopes").String()),
AuthUrl: sec.Key("auth_url").String(),
TokenUrl: sec.Key("token_url").String(),
ApiUrl: sec.Key("api_url").String(),
TeamsUrl: sec.Key("teams_url").String(),
Enabled: sec.Key("enabled").MustBool(),
EmailAttributeName: sec.Key("email_attribute_name").String(),
EmailAttributePath: sec.Key("email_attribute_path").String(),
RoleAttributePath: sec.Key("role_attribute_path").String(),
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
GroupsAttributePath: sec.Key("groups_attribute_path").String(),
TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name),
Icon: sec.Key("icon").String(),
TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
UsePKCE: sec.Key("use_pkce").MustBool(),
ClientId: sec.Key("client_id").String(),
ClientSecret: sec.Key("client_secret").String(),
Scopes: util.SplitString(sec.Key("scopes").String()),
AuthUrl: sec.Key("auth_url").String(),
TokenUrl: sec.Key("token_url").String(),
ApiUrl: sec.Key("api_url").String(),
TeamsUrl: sec.Key("teams_url").String(),
Enabled: sec.Key("enabled").MustBool(),
EmailAttributeName: sec.Key("email_attribute_name").String(),
EmailAttributePath: sec.Key("email_attribute_path").String(),
RoleAttributePath: sec.Key("role_attribute_path").String(),
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
GroupsAttributePath: sec.Key("groups_attribute_path").String(),
TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name),
Icon: sec.Key("icon").String(),
TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
UsePKCE: sec.Key("use_pkce").MustBool(),
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false),
}
// when empty_scopes parameter exists and is true, overwrite scope with empty value
@@ -163,21 +167,17 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
// AzureAD.
if name == "azuread" {
ss.socialMap["azuread"] = &SocialAzureAD{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole),
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
autoAssignOrgRole: cfg.AutoAssignOrgRole,
roleAttributeStrict: info.RoleAttributeStrict,
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole),
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
}
}
// Okta
if name == "okta" {
ss.socialMap["okta"] = &SocialOkta{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole),
apiUrl: info.ApiUrl,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole),
apiUrl: info.ApiUrl,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
}
}
@@ -190,8 +190,6 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
nameAttributePath: sec.Key("name_attribute_path").String(),
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
groupsAttributePath: info.GroupsAttributePath,
loginAttributePath: sec.Key("login_attribute_path").String(),
idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
@@ -226,18 +224,18 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
}
type BasicUserInfo struct {
Id string
Name string
Email string
Login string
Company string
Role string
Groups []string
Id string
Name string
Email string
Login string
Role models.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, Company: %s, Role: %s, Groups: %v",
b.Id, b.Name, b.Email, b.Login, b.Company, b.Role, b.Groups)
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)
}
type SocialConnector interface {
@@ -254,9 +252,10 @@ type SocialConnector interface {
type SocialBase struct {
*oauth2.Config
log log.Logger
allowSignup bool
allowedDomains []string
log log.Logger
allowSignup bool
allowAssignGrafanaAdmin bool
allowedDomains []string
roleAttributePath string
roleAttributeStrict bool
@@ -272,7 +271,8 @@ func (e Error) Error() string {
}
const (
grafanaCom = "grafana_com"
grafanaCom = "grafana_com"
RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces
)
var (
@@ -297,13 +297,14 @@ func newSocialBase(name string,
logger := log.New("oauth." + name)
return &SocialBase{
Config: config,
log: logger,
allowSignup: info.AllowSignup,
allowedDomains: info.AllowedDomains,
autoAssignOrgRole: autoAssignOrgRole,
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
Config: config,
log: logger,
allowSignup: info.AllowSignup,
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
allowedDomains: info.AllowedDomains,
autoAssignOrgRole: autoAssignOrgRole,
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
}
}
@@ -311,28 +312,56 @@ type groupStruct struct {
Groups []string `json:"groups"`
}
func (s *SocialBase) extractRole(rawJSON []byte, groups []string) (models.RoleType, error) {
func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string, legacy bool) (models.RoleType, bool) {
if s.roleAttributePath == "" {
if s.autoAssignOrgRole != "" {
return models.RoleType(s.autoAssignOrgRole), nil
}
return "", nil
return s.defaultRole(legacy), false
}
role, err := s.searchJSONForStringAttr(s.roleAttributePath, rawJSON)
if err == nil && role != "" {
return models.RoleType(role), nil
return getRoleFromSearch(role)
}
if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil {
if role, err := s.searchJSONForStringAttr(
s.roleAttributePath, groupBytes); err == nil && role != "" {
return models.RoleType(role), nil
role, err := s.searchJSONForStringAttr(s.roleAttributePath, groupBytes)
if err == nil && role != "" {
return getRoleFromSearch(role)
}
}
return "", nil
return s.defaultRole(legacy), false
}
// 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(legacy bool) models.RoleType {
if s.roleAttributeStrict {
s.log.Debug("RoleAttributeStrict is set, returning no role.")
return ""
}
if s.autoAssignOrgRole != "" && !legacy {
s.log.Debug("No role found, returning default.")
return models.RoleType(s.autoAssignOrgRole)
}
if legacy {
s.log.Warn("No valid role found. Skipping role sync. " +
"In Grafana 10, this will result in the user being assigned the default role and overriding manual assignment. " +
"If role sync is not desired, set oauth_skip_org_role_update_sync to false")
}
return ""
}
// match grafana admin role and translate to org role and bool.
// treat the JSON search result to ensure correct casing.
func getRoleFromSearch(role string) (models.RoleType, bool) {
if strings.EqualFold(role, RoleGrafanaAdmin) {
return models.ROLE_ADMIN, true
}
return models.RoleType(cases.Title(language.Und).String(role)), false
}
// GetOAuthProviders returns available oauth providers and if they're enabled or not