Auth: Implement the reload functionality from OAuth social connectors (#79795)

* implement Reload() func for azuread provider

* add unit test for failure

* use mutex when updating the info field

* implement the Reload() func for the other providers

* use mutex when reading info

* retrieve info using GetOAuthInfo() in common file

* move Reload() to SocialBase
This commit is contained in:
Mihai Doarna 2024-01-12 10:25:51 +02:00 committed by GitHub
parent 39e4f8ec1b
commit 1807435d9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 584 additions and 108 deletions

View File

@ -113,10 +113,12 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
return nil, ErrEmailNotFound return nil, ErrEmailNotFound
} }
info := s.GetOAuthInfo()
// setting the role, grafanaAdmin to empty to reflect that we are not syncronizing with the external provider // setting the role, grafanaAdmin to empty to reflect that we are not syncronizing with the external provider
var role roletype.RoleType var role roletype.RoleType
var grafanaAdmin bool var grafanaAdmin bool
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
role, grafanaAdmin, err = s.extractRoleAndAdmin(claims) role, grafanaAdmin, err = s.extractRoleAndAdmin(claims)
if err != nil { if err != nil {
return nil, err return nil, err
@ -143,11 +145,11 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
} }
var isGrafanaAdmin *bool = nil var isGrafanaAdmin *bool = nil
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin isGrafanaAdmin = &grafanaAdmin
} }
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
} }
@ -178,14 +180,6 @@ func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSett
return nil return nil
} }
func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialAzureAD) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) { func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) {
claims, err := s.validateIDTokenSignature(ctx, client, parsedToken) claims, err := s.validateIDTokenSignature(ctx, client, parsedToken)
if err != nil { if err != nil {
@ -257,8 +251,10 @@ 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) extractRoleAndAdmin(claims *azureClaims) (org.RoleType, bool, error) {
info := s.GetOAuthInfo()
if len(claims.Roles) == 0 { if len(claims.Roles) == 0 {
if s.info.RoleAttributeStrict { if info.RoleAttributeStrict {
return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: unset role") return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: unset role")
} }
return s.defaultRole(), false, nil return s.defaultRole(), false, nil
@ -276,7 +272,7 @@ func (s *SocialAzureAD) extractRoleAndAdmin(claims *azureClaims) (org.RoleType,
} }
} }
if s.info.RoleAttributeStrict { if info.RoleAttributeStrict {
return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: idP did not return a valid role %q", claims.Roles) return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: idP did not return a valid role %q", claims.Roles)
} }
@ -400,9 +396,11 @@ func (s *SocialAzureAD) groupsGraphAPIURL(claims *azureClaims, token *oauth2.Tok
} }
func (s *SocialAzureAD) SupportBundleContent(bf *bytes.Buffer) error { func (s *SocialAzureAD) SupportBundleContent(bf *bytes.Buffer) error {
info := s.GetOAuthInfo()
bf.WriteString("## AzureAD specific configuration\n\n") bf.WriteString("## AzureAD specific configuration\n\n")
bf.WriteString("```ini\n") bf.WriteString("```ini\n")
bf.WriteString(fmt.Sprintf("allowed_groups = %v\n", s.info.AllowedGroups)) bf.WriteString(fmt.Sprintf("allowed_groups = %v\n", info.AllowedGroups))
bf.WriteString(fmt.Sprintf("forceUseGraphAPI = %v\n", s.forceUseGraphAPI)) bf.WriteString(fmt.Sprintf("forceUseGraphAPI = %v\n", s.forceUseGraphAPI))
bf.WriteString("```\n\n") bf.WriteString("```\n\n")

View File

@ -1045,3 +1045,67 @@ func TestSocialAzureAD_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialAzureAD_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewAzureADProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -38,11 +38,15 @@ type httpGetResponse struct {
} }
func (s *SocialBase) IsEmailAllowed(email string) bool { func (s *SocialBase) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.info.AllowedDomains) info := s.GetOAuthInfo()
return isEmailAllowed(email, info.AllowedDomains)
} }
func (s *SocialBase) IsSignupAllowed() bool { func (s *SocialBase) IsSignupAllowed() bool {
return s.info.AllowSignup info := s.GetOAuthInfo()
return info.AllowSignup
} }
func isEmailAllowed(email string, allowedDomains []string) bool { func isEmailAllowed(email string, allowedDomains []string) bool {

View File

@ -84,17 +84,15 @@ func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SS
return nil return nil
} }
func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
// TODOD: remove this in the next PR and use the isGroupMember from social.go // TODOD: remove this in the next PR and use the isGroupMember from social.go
func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool { func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool {
if len(s.info.AllowedGroups) == 0 { info := s.GetOAuthInfo()
if len(info.AllowedGroups) == 0 {
return true return true
} }
for _, allowedGroup := range s.info.AllowedGroups { for _, allowedGroup := range info.AllowedGroups {
for _, group := range groups { for _, group := range groups {
if group == allowedGroup { if group == allowedGroup {
return true return true
@ -177,6 +175,8 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
toCheck = append(toCheck, apiData) toCheck = append(toCheck, apiData)
} }
info := s.GetOAuthInfo()
userInfo := &social.BasicUserInfo{} userInfo := &social.BasicUserInfo{}
for _, data := range toCheck { for _, data := range toCheck {
s.log.Debug("Processing external user info", "source", data.source, "data", data) s.log.Debug("Processing external user info", "source", data.source, "data", data)
@ -200,13 +200,13 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
} }
} }
if userInfo.Role == "" && !s.info.SkipOrgRoleSync { if userInfo.Role == "" && !info.SkipOrgRoleSync {
role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{}) role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{})
if err != nil { if err != nil {
s.log.Warn("Failed to extract role", "err", err) s.log.Warn("Failed to extract role", "err", err)
} else { } else {
userInfo.Role = role userInfo.Role = role
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin userInfo.IsGrafanaAdmin = &grafanaAdmin
} }
} }
@ -223,14 +223,14 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
} }
} }
if userInfo.Role == "" && !s.info.SkipOrgRoleSync { if userInfo.Role == "" && !info.SkipOrgRoleSync {
if s.info.RoleAttributeStrict { if info.RoleAttributeStrict {
return nil, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute") return nil, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute")
} }
userInfo.Role = s.defaultRole() userInfo.Role = s.defaultRole()
} }
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
} }
@ -264,10 +264,6 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
return userInfo, nil return userInfo, nil
} }
func (s *SocialGenericOAuth) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson { func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson {
s.log.Debug("Extracting user info from OAuth token") s.log.Debug("Extracting user info from OAuth token")
@ -302,15 +298,17 @@ func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson
} }
func (s *SocialGenericOAuth) extractFromAPI(ctx context.Context, client *http.Client) *UserInfoJson { func (s *SocialGenericOAuth) extractFromAPI(ctx context.Context, client *http.Client) *UserInfoJson {
info := s.GetOAuthInfo()
s.log.Debug("Getting user info from API") s.log.Debug("Getting user info from API")
if s.info.ApiUrl == "" { if info.ApiUrl == "" {
s.log.Debug("No api url configured") s.log.Debug("No api url configured")
return nil return nil
} }
rawUserInfoResponse, err := s.httpGet(ctx, client, s.info.ApiUrl) rawUserInfoResponse, err := s.httpGet(ctx, client, info.ApiUrl)
if err != nil { if err != nil {
s.log.Debug("Error getting user info from API", "url", s.info.ApiUrl, "error", err) s.log.Debug("Error getting user info from API", "url", info.ApiUrl, "error", err)
return nil return nil
} }
@ -426,9 +424,11 @@ func (s *SocialGenericOAuth) FetchPrivateEmail(ctx context.Context, client *http
IsConfirmed bool `json:"is_confirmed"` IsConfirmed bool `json:"is_confirmed"`
} }
response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/emails")) info := s.GetOAuthInfo()
response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/emails"))
if err != nil { if err != nil {
s.log.Error("Error getting email address", "url", s.info.ApiUrl+"/emails", "error", err) s.log.Error("Error getting email address", "url", info.ApiUrl+"/emails", "error", err)
return "", fmt.Errorf("%v: %w", "Error getting email address", err) return "", fmt.Errorf("%v: %w", "Error getting email address", err)
} }
@ -488,9 +488,11 @@ func (s *SocialGenericOAuth) fetchTeamMembershipsFromDeprecatedTeamsUrl(ctx cont
Id int `json:"id"` Id int `json:"id"`
} }
response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/teams")) info := s.GetOAuthInfo()
response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/teams"))
if err != nil { if err != nil {
s.log.Error("Error getting team memberships", "url", s.info.ApiUrl+"/teams", "error", err) s.log.Error("Error getting team memberships", "url", info.ApiUrl+"/teams", "error", err)
return []string{}, err return []string{}, err
} }
@ -529,9 +531,11 @@ func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *htt
Login string `json:"login"` Login string `json:"login"`
} }
response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/orgs")) info := s.GetOAuthInfo()
response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/orgs"))
if err != nil { if err != nil {
s.log.Error("Error getting organizations", "url", s.info.ApiUrl+"/orgs", "error", err) s.log.Error("Error getting organizations", "url", info.ApiUrl+"/orgs", "error", err)
return nil, false return nil, false
} }

View File

@ -973,3 +973,67 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialGenericOAuth_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGenericOAuthProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -91,10 +91,6 @@ func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSetti
return nil return nil
} }
func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool { func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool {
if len(s.teamIds) == 0 { if len(s.teamIds) == 0 {
return true return true
@ -145,7 +141,9 @@ func (s *SocialGithub) FetchPrivateEmail(ctx context.Context, client *http.Clien
Verified bool `json:"verified"` Verified bool `json:"verified"`
} }
response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/emails")) info := s.GetOAuthInfo()
response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/emails"))
if err != nil { if err != nil {
return "", fmt.Errorf("Error getting email address: %s", err) return "", fmt.Errorf("Error getting email address: %s", err)
} }
@ -168,7 +166,9 @@ func (s *SocialGithub) FetchPrivateEmail(ctx context.Context, client *http.Clien
} }
func (s *SocialGithub) FetchTeamMemberships(ctx context.Context, client *http.Client) ([]GithubTeam, error) { func (s *SocialGithub) FetchTeamMemberships(ctx context.Context, client *http.Client) ([]GithubTeam, error) {
url := fmt.Sprintf(s.info.ApiUrl + "/teams?per_page=100") info := s.GetOAuthInfo()
url := fmt.Sprintf(info.ApiUrl + "/teams?per_page=100")
hasMore := true hasMore := true
teams := make([]GithubTeam, 0) teams := make([]GithubTeam, 0)
@ -250,7 +250,9 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token
Name string `json:"name"` Name string `json:"name"`
} }
response, err := s.httpGet(ctx, client, s.info.ApiUrl) info := s.GetOAuthInfo()
response, err := s.httpGet(ctx, client, info.ApiUrl)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting user info: %s", err) return nil, fmt.Errorf("error getting user info: %s", err)
} }
@ -269,20 +271,20 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token
var role roletype.RoleType var role roletype.RoleType
var isGrafanaAdmin *bool = nil var isGrafanaAdmin *bool = nil
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
var grafanaAdmin bool var grafanaAdmin bool
role, grafanaAdmin, err = s.extractRoleAndAdmin(response.Body, teams) role, grafanaAdmin, err = s.extractRoleAndAdmin(response.Body, teams)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin isGrafanaAdmin = &grafanaAdmin
} }
} }
// we skip allowing assignment of GrafanaAdmin if skipOrgRoleSync is present // we skip allowing assignment of GrafanaAdmin if skipOrgRoleSync is present
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
} }
@ -299,7 +301,7 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token
userInfo.Name = data.Name userInfo.Name = data.Name
} }
organizationsUrl := fmt.Sprintf(s.info.ApiUrl + "/orgs?per_page=100") organizationsUrl := fmt.Sprintf(info.ApiUrl + "/orgs?per_page=100")
if !s.IsTeamMember(ctx, client) { if !s.IsTeamMember(ctx, client) {
return nil, ErrMissingTeamMembership.Errorf("User is not a member of any of the allowed teams: %v", s.teamIds) return nil, ErrMissingTeamMembership.Errorf("User is not a member of any of the allowed teams: %v", s.teamIds)
@ -328,10 +330,6 @@ func (t *GithubTeam) GetShorthand() (string, error) {
return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil
} }
func (s *SocialGithub) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
func convertToGroupList(t []GithubTeam) []string { func convertToGroupList(t []GithubTeam) []string {
groups := make([]string, 0) groups := make([]string, 0)
for _, team := range t { for _, team := range t {

View File

@ -399,3 +399,67 @@ func TestSocialGitHub_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialGitHub_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGitHubProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -81,10 +81,6 @@ func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSetti
return nil return nil
} }
func (s *SocialGitlab) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string { func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
groups := make([]string, 0) groups := make([]string, 0)
nextPage := new(int) nextPage := new(int)
@ -104,7 +100,9 @@ func (s *SocialGitlab) getGroupsPage(ctx context.Context, client *http.Client, n
FullPath string `json:"full_path"` FullPath string `json:"full_path"`
} }
groupURL, err := url.JoinPath(s.info.ApiUrl, "/groups") info := s.GetOAuthInfo()
groupURL, err := url.JoinPath(info.ApiUrl, "/groups")
if err != nil { if err != nil {
s.log.Error("Error joining GitLab API URL", "err", err) s.log.Error("Error joining GitLab API URL", "err", err)
return nil, nil return nil, nil
@ -165,6 +163,8 @@ func (s *SocialGitlab) getGroupsPage(ctx context.Context, client *http.Client, n
} }
func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
info := s.GetOAuthInfo()
data, err := s.extractFromToken(ctx, client, token) data, err := s.extractFromToken(ctx, client, token)
if err != nil { if err != nil {
return nil, err return nil, err
@ -193,20 +193,18 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token
return nil, errMissingGroupMembership return nil, errMissingGroupMembership
} }
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
} }
return userInfo, nil return userInfo, nil
} }
func (s *SocialGitlab) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) { func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
info := s.GetOAuthInfo()
apiResp := &apiData{} apiResp := &apiData{}
response, err := s.httpGet(ctx, client, s.info.ApiUrl+"/user") response, err := s.httpGet(ctx, client, info.ApiUrl+"/user")
if err != nil { if err != nil {
return nil, fmt.Errorf("Error getting user info: %w", err) return nil, fmt.Errorf("Error getting user info: %w", err)
} }
@ -232,14 +230,14 @@ func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client,
Groups: s.getGroups(ctx, client), Groups: s.getGroups(ctx, client),
} }
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
var grafanaAdmin bool var grafanaAdmin bool
role, grafanaAdmin, err := s.extractRoleAndAdmin(response.Body, idData.Groups) role, grafanaAdmin, err := s.extractRoleAndAdmin(response.Body, idData.Groups)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
idData.IsGrafanaAdmin = &grafanaAdmin idData.IsGrafanaAdmin = &grafanaAdmin
} }
@ -256,6 +254,8 @@ func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client,
func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) { func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
s.log.Debug("Extracting user info from OAuth token") s.log.Debug("Extracting user info from OAuth token")
info := s.GetOAuthInfo()
idToken := token.Extra("id_token") idToken := token.Extra("id_token")
if idToken == nil { if idToken == nil {
s.log.Debug("No id_token found, defaulting to API access", "token", token) s.log.Debug("No id_token found, defaulting to API access", "token", token)
@ -289,13 +289,13 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client
data.Groups = userInfo.Groups data.Groups = userInfo.Groups
} }
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
role, grafanaAdmin, errRole := s.extractRoleAndAdmin(rawJSON, data.Groups) role, grafanaAdmin, errRole := s.extractRoleAndAdmin(rawJSON, data.Groups)
if errRole != nil { if errRole != nil {
return nil, errRole return nil, errRole
} }
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
data.IsGrafanaAdmin = &grafanaAdmin data.IsGrafanaAdmin = &grafanaAdmin
} }

View File

@ -517,3 +517,67 @@ func TestSocialGitlab_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialGitlab_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGitLabProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -71,11 +71,9 @@ func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSetti
return nil return nil
} }
func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
info := s.GetOAuthInfo()
data, errToken := s.extractFromToken(ctx, client, token) data, errToken := s.extractFromToken(ctx, client, token)
if errToken != nil { if errToken != nil {
return nil, errToken return nil, errToken
@ -116,13 +114,13 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
Groups: groups, Groups: groups,
} }
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
role, grafanaAdmin, errRole := s.extractRoleAndAdmin(data.rawJSON, groups) role, grafanaAdmin, errRole := s.extractRoleAndAdmin(data.rawJSON, groups)
if errRole != nil { if errRole != nil {
return nil, errRole return nil, errRole
} }
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin userInfo.IsGrafanaAdmin = &grafanaAdmin
} }
@ -134,10 +132,6 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
return userInfo, nil return userInfo, nil
} }
func (s *SocialGoogle) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
type googleAPIData struct { type googleAPIData struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -146,9 +140,11 @@ type googleAPIData struct {
} }
func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) { func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) {
if strings.HasPrefix(s.info.ApiUrl, legacyAPIURL) { info := s.GetOAuthInfo()
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {
data := googleAPIData{} data := googleAPIData{}
response, err := s.httpGet(ctx, client, s.info.ApiUrl) response, err := s.httpGet(ctx, client, info.ApiUrl)
if err != nil { if err != nil {
return nil, fmt.Errorf("error retrieving legacy user info: %s", err) return nil, fmt.Errorf("error retrieving legacy user info: %s", err)
} }
@ -167,7 +163,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
} }
data := googleUserData{} data := googleUserData{}
response, err := s.httpGet(ctx, client, s.info.ApiUrl) response, err := s.httpGet(ctx, client, info.ApiUrl)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting user info: %s", err) return nil, fmt.Errorf("error getting user info: %s", err)
} }
@ -180,7 +176,9 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
} }
func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.info.UseRefreshToken { info := s.GetOAuthInfo()
if info.UseRefreshToken {
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce) opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
} }
return s.SocialBase.AuthCodeURL(state, opts...) return s.SocialBase.AuthCodeURL(state, opts...)

View File

@ -722,3 +722,67 @@ func TestSocialGoogle_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialGoogle_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGoogleProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -69,10 +69,6 @@ func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOS
return nil return nil
} }
func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool { func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool {
return true return true
} }
@ -104,6 +100,8 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
Orgs []OrgRecord `json:"orgs"` Orgs []OrgRecord `json:"orgs"`
} }
info := s.GetOAuthInfo()
response, err := s.httpGet(ctx, client, s.url+"/api/oauth2/user") response, err := s.httpGet(ctx, client, s.url+"/api/oauth2/user")
if err != nil { if err != nil {
@ -117,7 +115,7 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
// on login we do not want to display the role from the external provider // on login we do not want to display the role from the external provider
var role roletype.RoleType var role roletype.RoleType
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
role = org.RoleType(data.Role) role = org.RoleType(data.Role)
} }
userInfo := &social.BasicUserInfo{ userInfo := &social.BasicUserInfo{
@ -136,7 +134,3 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
return userInfo, nil return userInfo, nil
} }
func (s *SocialGrafanaCom) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -190,3 +190,76 @@ func TestSocialGrafanaCom_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialGrafanaCom_Reload(t *testing.T) {
const GrafanaComURL = "http://localhost:3000"
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"name": "a-new-name",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
Name: "a-new-name",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
// these are the overwrites from the constructor
AuthUrl: GrafanaComURL + "/oauth2/authorize",
TokenUrl: GrafanaComURL + "/api/oauth2/token",
AuthStyle: "inheader",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cfg := &setting.Cfg{
GrafanaComURL: GrafanaComURL,
}
s := NewGrafanaComProvider(tc.info, cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -77,10 +77,6 @@ func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSetting
return nil return nil
} }
func (s *SocialOkta) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (claims *OktaClaims) extractEmail() string { func (claims *OktaClaims) extractEmail() string {
if claims.Email == "" && claims.PreferredUsername != "" { if claims.Email == "" && claims.PreferredUsername != "" {
return claims.PreferredUsername return claims.PreferredUsername
@ -90,6 +86,8 @@ func (claims *OktaClaims) extractEmail() string {
} }
func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
info := s.GetOAuthInfo()
idToken := token.Extra("id_token") idToken := token.Extra("id_token")
if idToken == nil { if idToken == nil {
return nil, fmt.Errorf("no id_token found") return nil, fmt.Errorf("no id_token found")
@ -123,18 +121,18 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
var role roletype.RoleType var role roletype.RoleType
var isGrafanaAdmin *bool var isGrafanaAdmin *bool
if !s.info.SkipOrgRoleSync { if !info.SkipOrgRoleSync {
var grafanaAdmin bool var grafanaAdmin bool
role, grafanaAdmin, err = s.extractRoleAndAdmin(data.rawJSON, groups) role, grafanaAdmin, err = s.extractRoleAndAdmin(data.rawJSON, groups)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if s.info.AllowAssignGrafanaAdmin { if info.AllowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin isGrafanaAdmin = &grafanaAdmin
} }
} }
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
} }
@ -149,14 +147,12 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
}, nil }, nil
} }
func (s *SocialOkta) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error { func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error {
rawUserInfoResponse, err := s.httpGet(ctx, client, s.info.ApiUrl) info := s.GetOAuthInfo()
rawUserInfoResponse, err := s.httpGet(ctx, client, info.ApiUrl)
if err != nil { if err != nil {
s.log.Debug("Error getting user info response", "url", s.info.ApiUrl, "error", err) s.log.Debug("Error getting user info response", "url", info.ApiUrl, "error", err)
return fmt.Errorf("error getting user info response: %w", err) return fmt.Errorf("error getting user info response: %w", err)
} }
data.rawJSON = rawUserInfoResponse.Body data.rawJSON = rawUserInfoResponse.Body
@ -182,11 +178,13 @@ func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string {
// TODO: remove this in a separate PR and use the isGroupMember from the social.go // TODO: remove this in a separate PR and use the isGroupMember from the social.go
func (s *SocialOkta) IsGroupMember(groups []string) bool { func (s *SocialOkta) IsGroupMember(groups []string) bool {
if len(s.info.AllowedGroups) == 0 { info := s.GetOAuthInfo()
if len(info.AllowedGroups) == 0 {
return true return true
} }
for _, allowedGroup := range s.info.AllowedGroups { for _, allowedGroup := range info.AllowedGroups {
for _, group := range groups { for _, group := range groups {
if group == allowedGroup { if group == allowedGroup {
return true return true

View File

@ -190,3 +190,67 @@ func TestSocialOkta_Validate(t *testing.T) {
}) })
} }
} }
func TestSocialOkta_Reload(t *testing.T) {
testCases := []struct {
name string
info *social.OAuthInfo
settings ssoModels.SSOSettings
expectError bool
expectedInfo *social.OAuthInfo
}{
{
name: "SSO provider successfully updated",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
},
},
expectError: false,
expectedInfo: &social.OAuthInfo{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
},
},
{
name: "fails if settings contain invalid values",
info: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": []string{"first", "second"},
},
},
expectError: true,
expectedInfo: &social.OAuthInfo{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewOktaProvider(tc.info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Reload(context.Background(), tc.settings)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.EqualValues(t, tc.expectedInfo, s.info)
})
}
}

View File

@ -3,12 +3,14 @@ package connectors
import ( import (
"bytes" "bytes"
"compress/zlib" "compress/zlib"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"regexp" "regexp"
"strings" "strings"
"sync"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/cases" "golang.org/x/text/cases"
@ -19,11 +21,13 @@ import (
"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"
"github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
) )
type SocialBase struct { type SocialBase struct {
*oauth2.Config *oauth2.Config
info *social.OAuthInfo info *social.OAuthInfo
infoMutex sync.RWMutex
log log.Logger log log.Logger
autoAssignOrgRole string autoAssignOrgRole string
features featuremgmt.FeatureToggles features featuremgmt.FeatureToggles
@ -71,6 +75,27 @@ func (s *SocialBase) SupportBundleContent(bf *bytes.Buffer) error {
return nil return nil
} }
func (s *SocialBase) GetOAuthInfo() *social.OAuthInfo {
s.infoMutex.RLock()
defer s.infoMutex.RUnlock()
return s.info
}
func (s *SocialBase) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return fmt.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
s.infoMutex.Lock()
defer s.infoMutex.Unlock()
s.info = info
return nil
}
func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string) (org.RoleType, bool, error) { func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
if s.info.RoleAttributePath == "" { if s.info.RoleAttributePath == "" {
if s.info.RoleAttributeStrict { if s.info.RoleAttributeStrict {