AuthN: Support reloading SSO config after the sso settings have changed (#80734)
* Add AuthNSvc reload handling * Working, need to add test * Remove commented out code * Add Reload implementation to connectors * Align and add tests, refactor * Add more tests, linting * Add extra checks + tests to oauth client * Clean up based on reviews * Move config instantiation into newSocialBase * Use specific error
This commit is contained in:
@@ -147,7 +147,7 @@ func (s *SSOSettingsService) ListWithRedactedSecrets(ctx context.Context) ([]*mo
|
||||
return storeSettings, nil
|
||||
}
|
||||
|
||||
func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSettings) error {
|
||||
func (s *SSOSettingsService) Upsert(ctx context.Context, settings *models.SSOSettings) error {
|
||||
if !isProviderConfigurable(settings.Provider) {
|
||||
return ssosettings.ErrInvalidProvider.Errorf("provider %s is not configurable", settings.Provider)
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSett
|
||||
return ssosettings.ErrInvalidProvider.Errorf("provider %s not found in reloadables", settings.Provider)
|
||||
}
|
||||
|
||||
err := social.Validate(ctx, settings)
|
||||
err := social.Validate(ctx, *settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -167,6 +167,8 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSett
|
||||
return err
|
||||
}
|
||||
|
||||
secrets := collectSecrets(settings, storedSettings)
|
||||
|
||||
settings.Settings, err = s.encryptSecrets(ctx, settings.Settings, storedSettings.Settings)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -178,7 +180,8 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSett
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = social.Reload(context.Background(), settings)
|
||||
settings.Settings = overrideMaps(storedSettings.Settings, settings.Settings, secrets)
|
||||
err = social.Reload(context.Background(), *settings)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to reload the provider", "provider", settings.Provider, "error", err)
|
||||
}
|
||||
@@ -249,7 +252,7 @@ func (s *SSOSettingsService) getFallbackStrategyFor(provider string) (ssosetting
|
||||
func (s *SSOSettingsService) encryptSecrets(ctx context.Context, settings, storedSettings map[string]any) (map[string]any, error) {
|
||||
result := make(map[string]any)
|
||||
for k, v := range settings {
|
||||
if isSecret(k) {
|
||||
if isSecret(k) && v != "" {
|
||||
strValue, ok := v.(string)
|
||||
if !ok {
|
||||
return result, fmt.Errorf("failed to encrypt %s setting because it is not a string: %v", k, v)
|
||||
@@ -315,20 +318,24 @@ func (s *SSOSettingsService) doReload(ctx context.Context) {
|
||||
|
||||
// mergeSSOSettings merges the settings from the database with the system settings
|
||||
// Required because it is possible that the user has configured some of the settings (current Advanced OAuth settings)
|
||||
// and the rest of the settings are loaded from the system settings
|
||||
// and the rest of the settings have to be loaded from the system settings
|
||||
func (s *SSOSettingsService) mergeSSOSettings(dbSettings, systemSettings *models.SSOSettings) *models.SSOSettings {
|
||||
if dbSettings == nil {
|
||||
s.logger.Debug("No SSO Settings found in the database, using system settings")
|
||||
return systemSettings
|
||||
}
|
||||
|
||||
s.logger.Debug("Merging SSO Settings", "dbSettings", dbSettings.Settings, "systemSettings", systemSettings.Settings)
|
||||
s.logger.Debug("Merging SSO Settings", "dbSettings", removeSecrets(dbSettings.Settings), "systemSettings", removeSecrets(systemSettings.Settings))
|
||||
|
||||
finalSettings := mergeSettings(dbSettings.Settings, systemSettings.Settings)
|
||||
result := &models.SSOSettings{
|
||||
Provider: dbSettings.Provider,
|
||||
Source: dbSettings.Source,
|
||||
Settings: mergeSettings(dbSettings.Settings, systemSettings.Settings),
|
||||
Created: dbSettings.Created,
|
||||
Updated: dbSettings.Updated,
|
||||
}
|
||||
|
||||
dbSettings.Settings = finalSettings
|
||||
|
||||
return dbSettings
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SSOSettingsService) decryptSecrets(ctx context.Context, settings map[string]any) (map[string]any, error) {
|
||||
@@ -358,6 +365,22 @@ func (s *SSOSettingsService) decryptSecrets(ctx context.Context, settings map[st
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// removeSecrets removes all the secrets from the map and replaces them with a redacted password
|
||||
// and returns a new map
|
||||
func removeSecrets(settings map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
for k, v := range settings {
|
||||
if isSecret(k) {
|
||||
result[k] = setting.RedactedPassword
|
||||
continue
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// mergeSettings merges two maps in a way that the values from the first map are preserved
|
||||
// and the values from the second map are added only if they don't exist in the first map
|
||||
func mergeSettings(storedSettings, systemSettings map[string]any) map[string]any {
|
||||
settings := make(map[string]any)
|
||||
|
||||
@@ -374,6 +397,32 @@ func mergeSettings(storedSettings, systemSettings map[string]any) map[string]any
|
||||
return settings
|
||||
}
|
||||
|
||||
// collectSecrets collects all the secrets from the request and the currently stored settings
|
||||
// and returns a new map
|
||||
func collectSecrets(settings *models.SSOSettings, storedSettings *models.SSOSettings) map[string]any {
|
||||
secrets := map[string]any{}
|
||||
for k, v := range settings.Settings {
|
||||
if isSecret(k) {
|
||||
if isNewSecretValue(v.(string)) {
|
||||
secrets[k] = v.(string) // use the new value
|
||||
continue
|
||||
}
|
||||
secrets[k] = storedSettings.Settings[k] // keep the currently stored value
|
||||
}
|
||||
}
|
||||
return secrets
|
||||
}
|
||||
|
||||
func overrideMaps(maps ...map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isSecret(fieldName string) bool {
|
||||
secretFieldPatterns := []string{"secret"}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -772,16 +775,50 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
},
|
||||
IsDeleted: false,
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
reloadable := ssosettingstests.NewMockReloadable(t)
|
||||
reloadable.On("Validate", mock.Anything, settings).Return(nil)
|
||||
reloadable.On("Reload", mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||
reloadable.On("Reload", mock.Anything, mock.MatchedBy(func(settings models.SSOSettings) bool {
|
||||
wg.Done()
|
||||
return settings.Provider == provider &&
|
||||
settings.ID == "someid" &&
|
||||
maps.Equal(settings.Settings, map[string]any{
|
||||
"client_id": "client-id",
|
||||
"client_secret": "client-secret",
|
||||
"enabled": true,
|
||||
})
|
||||
})).Return(nil).Once()
|
||||
env.reloadables[provider] = reloadable
|
||||
env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once()
|
||||
env.secrets.On("Decrypt", mock.Anything, []byte("encrypted-current-client-secret"), mock.Anything).Return([]byte("current-client-secret"), nil).Once()
|
||||
|
||||
err := env.service.Upsert(context.Background(), settings)
|
||||
env.store.UpsertFn = func(ctx context.Context, settings *models.SSOSettings) error {
|
||||
currentTime := time.Now()
|
||||
settings.ID = "someid"
|
||||
settings.Created = currentTime
|
||||
settings.Updated = currentTime
|
||||
|
||||
env.store.ActualSSOSettings = *settings
|
||||
return nil
|
||||
}
|
||||
|
||||
env.store.GetFn = func(ctx context.Context, provider string) (*models.SSOSettings, error) {
|
||||
return &models.SSOSettings{
|
||||
ID: "someid",
|
||||
Provider: provider,
|
||||
Settings: map[string]any{
|
||||
"client_secret": base64.RawStdEncoding.EncodeToString([]byte("encrypted-current-client-secret")),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
err := env.service.Upsert(context.Background(), &settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the goroutine first to assert the Reload call
|
||||
wg.Wait()
|
||||
|
||||
settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-client-secret"))
|
||||
require.EqualValues(t, settings, env.store.ActualSSOSettings)
|
||||
})
|
||||
@@ -790,7 +827,7 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
|
||||
provider := social.GrafanaComProviderName
|
||||
settings := models.SSOSettings{
|
||||
settings := &models.SSOSettings{
|
||||
Provider: provider,
|
||||
Settings: map[string]any{
|
||||
"client_id": "client-id",
|
||||
@@ -811,7 +848,7 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
|
||||
provider := social.AzureADProviderName
|
||||
settings := models.SSOSettings{
|
||||
settings := &models.SSOSettings{
|
||||
Provider: provider,
|
||||
Settings: map[string]any{
|
||||
"client_id": "client-id",
|
||||
@@ -847,14 +884,14 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
reloadable.On("Validate", mock.Anything, settings).Return(errors.New("validation failed"))
|
||||
env.reloadables[provider] = reloadable
|
||||
|
||||
err := env.service.Upsert(context.Background(), settings)
|
||||
err := env.service.Upsert(context.Background(), &settings)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns error if a fallback strategy is not available for the provider", func(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
|
||||
settings := models.SSOSettings{
|
||||
settings := &models.SSOSettings{
|
||||
Provider: social.AzureADProviderName,
|
||||
Settings: map[string]any{
|
||||
"client_id": "client-id",
|
||||
@@ -889,7 +926,7 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
env.reloadables[provider] = reloadable
|
||||
env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return(nil, errors.New("encryption failed")).Once()
|
||||
|
||||
err := env.service.Upsert(context.Background(), settings)
|
||||
err := env.service.Upsert(context.Background(), &settings)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
@@ -921,7 +958,7 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
env.secrets.On("Decrypt", mock.Anything, []byte("current-client-secret"), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once()
|
||||
env.secrets.On("Encrypt", mock.Anything, []byte("encrypted-client-secret"), mock.Anything).Return([]byte("current-client-secret"), nil).Once()
|
||||
|
||||
err := env.service.Upsert(context.Background(), settings)
|
||||
err := env.service.Upsert(context.Background(), &settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("current-client-secret"))
|
||||
@@ -950,11 +987,11 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
return &models.SSOSettings{}, nil
|
||||
}
|
||||
|
||||
env.store.UpsertFn = func(ctx context.Context, settings models.SSOSettings) error {
|
||||
env.store.UpsertFn = func(ctx context.Context, settings *models.SSOSettings) error {
|
||||
return errors.New("failed to upsert settings")
|
||||
}
|
||||
|
||||
err := env.service.Upsert(context.Background(), settings)
|
||||
err := env.service.Upsert(context.Background(), &settings)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
@@ -978,7 +1015,7 @@ func TestSSOSettingsService_Upsert(t *testing.T) {
|
||||
env.reloadables[provider] = reloadable
|
||||
env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once()
|
||||
|
||||
err := env.service.Upsert(context.Background(), settings)
|
||||
err := env.service.Upsert(context.Background(), &settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-client-secret"))
|
||||
@@ -1098,6 +1135,22 @@ func TestSSOSettingsService_decryptSecrets(t *testing.T) {
|
||||
"other_secret": "decrypted-other-secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not decrypt when a secret is empty",
|
||||
setup: func(env testEnv) {
|
||||
env.secrets.On("Decrypt", mock.Anything, []byte("other_secret"), mock.Anything).Return([]byte("decrypted-other-secret"), nil).Once()
|
||||
},
|
||||
settings: map[string]any{
|
||||
"enabled": true,
|
||||
"client_secret": "",
|
||||
"other_secret": base64.RawStdEncoding.EncodeToString([]byte("other_secret")),
|
||||
},
|
||||
want: map[string]any{
|
||||
"enabled": true,
|
||||
"client_secret": "",
|
||||
"other_secret": "decrypted-other-secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return an error if data is not a string",
|
||||
settings: map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user