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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(","),
|
||||
|
||||
@@ -9,6 +9,7 @@ type OAuthInfo struct {
|
||||
EmailAttributePath string
|
||||
RoleAttributePath string
|
||||
RoleAttributeStrict bool
|
||||
GroupsAttributePath string
|
||||
AllowedDomains []string
|
||||
HostedDomain string
|
||||
ApiUrl string
|
||||
|
||||
Reference in New Issue
Block a user