mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
OAuth: Generic OAuth role mapping support (#17149)
Adds support for Generic OAuth role mapping. A new configuration setting for generic oauth is added named role_attribute_path which accepts a JMESPath expression. Only Grafana roles named Viewer, Editor or Admin are accepted. Closes #9766
This commit is contained in:
committed by
Marcus Efraimsson
parent
17f36d0492
commit
7a3d1c0e4b
@@ -184,7 +184,10 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
|
||||
}
|
||||
|
||||
if userInfo.Role != "" {
|
||||
extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
|
||||
rt := m.RoleType(userInfo.Role)
|
||||
if rt.IsValid() {
|
||||
extUser.OrgRoles[1] = rt
|
||||
}
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
|
||||
@@ -22,6 +22,7 @@ type SocialGenericOAuth struct {
|
||||
allowSignup bool
|
||||
emailAttributeName string
|
||||
emailAttributePath string
|
||||
roleAttributePath string
|
||||
teamIds []int
|
||||
}
|
||||
|
||||
@@ -79,13 +80,13 @@ func (s *SocialGenericOAuth) IsOrganizationMember(client *http.Client) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// searchJSONForEmail searches the provided JSON response for an e-mail address
|
||||
// using the configured e-mail attribute path associated with the generic OAuth
|
||||
// searchJSONForAttr searches the provided JSON response for the given attribute
|
||||
// using the configured attribute path associated with the generic OAuth
|
||||
// provider.
|
||||
// Returns an empty string if an e-mail address is not found.
|
||||
func (s *SocialGenericOAuth) searchJSONForEmail(data []byte) string {
|
||||
if s.emailAttributePath == "" {
|
||||
s.log.Error("No e-mail attribute path specified")
|
||||
// Returns an empty string if an attribute is not found.
|
||||
func (s *SocialGenericOAuth) searchJSONForAttr(attributePath string, data []byte) string {
|
||||
if attributePath == "" {
|
||||
s.log.Error("No attribute path specified")
|
||||
return ""
|
||||
}
|
||||
if len(data) == 0 {
|
||||
@@ -97,16 +98,16 @@ func (s *SocialGenericOAuth) searchJSONForEmail(data []byte) string {
|
||||
s.log.Error("Failed to unmarshal user info JSON response", "err", err.Error())
|
||||
return ""
|
||||
}
|
||||
val, err := jmespath.Search(s.emailAttributePath, buf)
|
||||
val, err := jmespath.Search(attributePath, buf)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to search user info JSON response with provided path", "emailAttributePath", s.emailAttributePath, "err", err.Error())
|
||||
s.log.Error("Failed to search user info JSON response with provided path", "attributePath", attributePath, "err", err.Error())
|
||||
return ""
|
||||
}
|
||||
strVal, ok := val.(string)
|
||||
if ok {
|
||||
return strVal
|
||||
}
|
||||
s.log.Error("E-mail not found when searching JSON with provided path", "emailAttributePath", s.emailAttributePath)
|
||||
s.log.Error("Attribute not found when searching JSON with provided path", "attributePath", attributePath)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -238,12 +239,15 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
|
||||
}
|
||||
}
|
||||
|
||||
role := s.extractRole(&data, rawUserInfoResponse.Body)
|
||||
|
||||
login := s.extractLogin(&data, email)
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Name: name,
|
||||
Login: login,
|
||||
Email: email,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
if !s.IsTeamMember(client) {
|
||||
@@ -298,7 +302,7 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson, userInfoResp []byt
|
||||
}
|
||||
|
||||
if s.emailAttributePath != "" {
|
||||
email := s.searchJSONForEmail(userInfoResp)
|
||||
email := s.searchJSONForAttr(s.emailAttributePath, userInfoResp)
|
||||
if email != "" {
|
||||
return email
|
||||
}
|
||||
@@ -320,6 +324,16 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson, userInfoResp []byt
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *SocialGenericOAuth) extractRole(data *UserInfoJson, userInfoResp []byte) string {
|
||||
if s.roleAttributePath != "" {
|
||||
role := s.searchJSONForAttr(s.roleAttributePath, userInfoResp)
|
||||
if role != "" {
|
||||
return role
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson, email string) string {
|
||||
if data.Login != "" {
|
||||
return data.Login
|
||||
|
||||
@@ -78,7 +78,61 @@ func TestSearchJSONForEmail(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
provider.emailAttributePath = test.EmailAttributePath
|
||||
Convey(test.Name, func() {
|
||||
actualResult := provider.searchJSONForEmail(test.UserInfoJSONResponse)
|
||||
actualResult := provider.searchJSONForAttr(test.EmailAttributePath, test.UserInfoJSONResponse)
|
||||
So(actualResult, ShouldEqual, test.ExpectedResult)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchJSONForRole(t *testing.T) {
|
||||
Convey("Given a generic OAuth provider", t, func() {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: log.New("generic_oauth_test"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
UserInfoJSONResponse []byte
|
||||
RoleAttributePath string
|
||||
ExpectedResult string
|
||||
}{
|
||||
{
|
||||
Name: "Given an invalid user info JSON response",
|
||||
UserInfoJSONResponse: []byte("{"),
|
||||
RoleAttributePath: "attributes.role",
|
||||
ExpectedResult: "",
|
||||
},
|
||||
{
|
||||
Name: "Given an empty user info JSON response and empty JMES path",
|
||||
UserInfoJSONResponse: []byte{},
|
||||
RoleAttributePath: "",
|
||||
ExpectedResult: "",
|
||||
},
|
||||
{
|
||||
Name: "Given an empty user info JSON response and valid JMES path",
|
||||
UserInfoJSONResponse: []byte{},
|
||||
RoleAttributePath: "attributes.role",
|
||||
ExpectedResult: "",
|
||||
},
|
||||
{
|
||||
Name: "Given a simple user info JSON response and valid JMES path",
|
||||
UserInfoJSONResponse: []byte(`{
|
||||
"attributes": {
|
||||
"role": "admin"
|
||||
}
|
||||
}`),
|
||||
RoleAttributePath: "attributes.role",
|
||||
ExpectedResult: "admin",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
provider.roleAttributePath = test.RoleAttributePath
|
||||
Convey(test.Name, func() {
|
||||
actualResult := provider.searchJSONForAttr(test.RoleAttributePath, test.UserInfoJSONResponse)
|
||||
So(actualResult, ShouldEqual, test.ExpectedResult)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ func NewOAuthService() {
|
||||
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(),
|
||||
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
@@ -169,6 +170,7 @@ func NewOAuthService() {
|
||||
allowSignup: info.AllowSignup,
|
||||
emailAttributeName: info.EmailAttributeName,
|
||||
emailAttributePath: info.EmailAttributePath,
|
||||
roleAttributePath: info.RoleAttributePath,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ type OAuthInfo struct {
|
||||
Enabled bool
|
||||
EmailAttributeName string
|
||||
EmailAttributePath string
|
||||
RoleAttributePath string
|
||||
AllowedDomains []string
|
||||
HostedDomain string
|
||||
ApiUrl string
|
||||
|
||||
Reference in New Issue
Block a user