Team Sync: Add group mapping to support team sync in the Generic OAuth provider (#36307)

Added group mapping to support team sync in the Generic OAuth provider.

Co-authored-by: Leonard Gram <leo@xlson.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Dan Cech <dan@aussiedan.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
Ward Bekker
2021-07-01 22:40:46 +02:00
committed by GitHub
parent d2f18f8b7d
commit b255f3db3f
9 changed files with 165 additions and 11 deletions
+31 -1
View File
@@ -74,7 +74,7 @@ func (s *SocialBase) httpGet(client *http.Client, url string) (response httpGetR
return
}
func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (string, error) {
func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (interface{}, error) {
if attributePath == "" {
return "", errors.New("no attribute path specified")
}
@@ -93,6 +93,15 @@ func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (strin
return "", errutil.Wrapf(err, "failed to search user info JSON response with provided path: %q", attributePath)
}
return val, nil
}
func (s *SocialBase) searchJSONForStringAttr(attributePath string, data []byte) (string, error) {
val, err := s.searchJSONForAttr(attributePath, data)
if err != nil {
return "", err
}
strVal, ok := val.(string)
if ok {
return strVal, nil
@@ -100,3 +109,24 @@ func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (strin
return "", nil
}
func (s *SocialBase) searchJSONForStringArrayAttr(attributePath string, data []byte) ([]string, error) {
val, err := s.searchJSONForAttr(attributePath, data)
if err != nil {
return []string{}, err
}
ifArr, ok := val.([]interface{})
if !ok {
return []string{}, nil
}
result := []string{}
for _, v := range ifArr {
if strVal, ok := v.(string); ok {
result = append(result, strVal)
}
}
return result, nil
}
+22 -4
View File
@@ -27,6 +27,7 @@ type SocialGenericOAuth struct {
nameAttributePath string
roleAttributePath string
roleAttributeStrict bool
groupsAttributePath string
idTokenAttributeName string
teamIds []int
}
@@ -119,7 +120,7 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
} else {
if s.loginAttributePath != "" {
s.log.Debug("Searching for login among JSON", "loginAttributePath", s.loginAttributePath)
login, err := s.searchJSONForAttr(s.loginAttributePath, data.rawJSON)
login, err := s.searchJSONForStringAttr(s.loginAttributePath, data.rawJSON)
if err != nil {
s.log.Error("Failed to search JSON for login attribute", "error", err)
} else if login != "" {
@@ -151,6 +152,14 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
userInfo.Role = role
}
}
groups, err := s.extractGroups(data)
if err != nil {
s.log.Error("Failed to extract groups", "error", err)
} else if len(groups) > 0 {
s.log.Debug("Setting user info groups from extracted groups")
userInfo.Groups = groups
}
}
if userInfo.Email == "" {
@@ -286,7 +295,7 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
}
if s.emailAttributePath != "" {
email, err := s.searchJSONForAttr(s.emailAttributePath, data.rawJSON)
email, err := s.searchJSONForStringAttr(s.emailAttributePath, data.rawJSON)
if err != nil {
s.log.Error("Failed to search JSON for attribute", "error", err)
} else if email != "" {
@@ -312,7 +321,7 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
func (s *SocialGenericOAuth) extractUserName(data *UserInfoJson) string {
if s.nameAttributePath != "" {
name, err := s.searchJSONForAttr(s.nameAttributePath, data.rawJSON)
name, err := s.searchJSONForStringAttr(s.nameAttributePath, data.rawJSON)
if err != nil {
s.log.Error("Failed to search JSON for attribute", "error", err)
} else if name != "" {
@@ -340,13 +349,22 @@ func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) (string, error) {
return "", nil
}
role, err := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
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
}
return s.searchJSONForStringArrayAttr(s.groupsAttributePath, data.rawJSON)
}
func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
type Record struct {
Email string `json:"email"`
+65 -2
View File
@@ -106,7 +106,70 @@ func TestSearchJSONForEmail(t *testing.T) {
for _, test := range tests {
provider.emailAttributePath = test.EmailAttributePath
t.Run(test.Name, func(t *testing.T) {
actualResult, err := provider.searchJSONForAttr(test.EmailAttributePath, test.UserInfoJSONResponse)
actualResult, err := provider.searchJSONForStringAttr(test.EmailAttributePath, test.UserInfoJSONResponse)
if test.ExpectedError == "" {
require.NoError(t, err, "Testing case %q", test.Name)
} else {
require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name)
}
require.Equal(t, test.ExpectedResult, actualResult)
})
}
})
}
func TestSearchJSONForGroups(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{
SocialBase: &SocialBase{
log: newLogger("generic_oauth_test", log15.LvlDebug),
},
}
tests := []struct {
Name string
UserInfoJSONResponse []byte
GroupsAttributePath string
ExpectedResult []string
ExpectedError string
}{
{
Name: "Given an invalid user info JSON response",
UserInfoJSONResponse: []byte("{"),
GroupsAttributePath: "attributes.groups",
ExpectedResult: []string{},
ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input",
},
{
Name: "Given an empty user info JSON response and empty JMES path",
UserInfoJSONResponse: []byte{},
GroupsAttributePath: "",
ExpectedResult: []string{},
ExpectedError: "no attribute path specified",
},
{
Name: "Given an empty user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte{},
GroupsAttributePath: "attributes.groups",
ExpectedResult: []string{},
ExpectedError: "empty user info JSON response provided",
},
{
Name: "Given a simple user info JSON response and valid JMES path",
UserInfoJSONResponse: []byte(`{
"attributes": {
"groups": ["foo", "bar"]
}
}`),
GroupsAttributePath: "attributes.groups[]",
ExpectedResult: []string{"foo", "bar"},
},
}
for _, test := range tests {
provider.groupsAttributePath = test.GroupsAttributePath
t.Run(test.Name, func(t *testing.T) {
actualResult, err := provider.searchJSONForStringArrayAttr(test.GroupsAttributePath, test.UserInfoJSONResponse)
if test.ExpectedError == "" {
require.NoError(t, err, "Testing case %q", test.Name)
} else {
@@ -169,7 +232,7 @@ func TestSearchJSONForRole(t *testing.T) {
for _, test := range tests {
provider.roleAttributePath = test.RoleAttributePath
t.Run(test.Name, func(t *testing.T) {
actualResult, err := provider.searchJSONForAttr(test.RoleAttributePath, test.UserInfoJSONResponse)
actualResult, err := provider.searchJSONForStringAttr(test.RoleAttributePath, test.UserInfoJSONResponse)
if test.ExpectedError == "" {
require.NoError(t, err, "Testing case %q", test.Name)
} else {
+1 -1
View File
@@ -125,7 +125,7 @@ func (s *SocialOkta) extractRole(data *OktaUserInfoJson) (string, error) {
return "", nil
}
role, err := s.searchJSONForAttr(s.roleAttributePath, data.rawJSON)
role, err := s.searchJSONForStringAttr(s.roleAttributePath, data.rawJSON)
if err != nil {
return "", err
}
+2
View File
@@ -98,6 +98,7 @@ func NewOAuthService(cfg *setting.Cfg) {
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(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
@@ -193,6 +194,7 @@ func NewOAuthService(cfg *setting.Cfg) {
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(),
teamIds: sec.Key("team_ids").Ints(","),
+1
View File
@@ -9,6 +9,7 @@ type OAuthInfo struct {
EmailAttributePath string
RoleAttributePath string
RoleAttributeStrict bool
GroupsAttributePath string
AllowedDomains []string
HostedDomain string
ApiUrl string