mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Introduce configurable_providers
config option for SSO settings (#80911)
* Add SSOSettingsConfigurableProviders config option * Add check to Delete and ListWithRedactedSecrets * Add check to GET, small improvements
This commit is contained in:
parent
4148362d63
commit
8246d97587
@ -584,6 +584,9 @@ id_response_header_namespaces = user api-key service-account
|
|||||||
# set to 0 to disable this feature
|
# set to 0 to disable this feature
|
||||||
reload_interval = 1m
|
reload_interval = 1m
|
||||||
|
|
||||||
|
# List of providers that can be configured through the SSO Settings API and UI.
|
||||||
|
configurable_providers = github gitlab google generic_oauth azuread okta
|
||||||
|
|
||||||
#################################### Anonymous Auth ######################
|
#################################### Anonymous Auth ######################
|
||||||
[auth.anonymous]
|
[auth.anonymous]
|
||||||
# enable anonymous access
|
# enable anonymous access
|
||||||
|
@ -3,7 +3,6 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -140,12 +139,8 @@ func (api *Api) getProviderSettings(c *contextmodel.ReqContext) response.Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider, err := api.SSOSettingsService.GetForProviderWithRedactedSecrets(c.Req.Context(), key)
|
provider, err := api.SSOSettingsService.GetForProviderWithRedactedSecrets(c.Req.Context(), key)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ssosettings.ErrNotFound) {
|
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to get provider settings", err)
|
||||||
return response.Error(http.StatusNotFound, "The provider was not found", err)
|
|
||||||
}
|
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to get provider settings", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
etag, err := generateFNVETag(provider)
|
etag, err := generateFNVETag(provider)
|
||||||
@ -214,10 +209,7 @@ func (api *Api) removeProviderSettings(c *contextmodel.ReqContext) response.Resp
|
|||||||
|
|
||||||
err := api.SSOSettingsService.Delete(c.Req.Context(), key)
|
err := api.SSOSettingsService.Delete(c.Req.Context(), key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ssosettings.ErrNotFound) {
|
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to delete provider settings", err)
|
||||||
return response.Error(http.StatusNotFound, "The provider was not found", err)
|
|
||||||
}
|
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to delete provider settings", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(http.StatusNoContent)
|
return response.Empty(http.StatusNoContent)
|
||||||
|
@ -336,6 +336,16 @@ func TestSSOSettingsAPI_GetForProvider(t *testing.T) {
|
|||||||
expectedServiceCall: true,
|
expectedServiceCall: true,
|
||||||
expectedStatusCode: http.StatusInternalServerError,
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "fails with not found error when the provider is not configurable",
|
||||||
|
key: "grafana_com",
|
||||||
|
action: "settings:read",
|
||||||
|
scope: "settings:*",
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: ssosettings.ErrNotConfigurable,
|
||||||
|
expectedServiceCall: true,
|
||||||
|
expectedStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package ssosettings
|
package ssosettings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = errors.New("not found")
|
errNotFoundBase = errutil.NotFound("sso.notFound", errutil.WithPublicMessage("The provider was not found."))
|
||||||
|
ErrNotFound = errNotFoundBase.Errorf("not found")
|
||||||
|
|
||||||
|
ErrNotConfigurable = errNotFoundBase.Errorf("not configurable")
|
||||||
|
|
||||||
ErrInvalidProvider = errutil.ValidationFailed("sso.invalidProvider", errutil.WithPublicMessage("Provider is invalid"))
|
ErrInvalidProvider = errutil.ValidationFailed("sso.invalidProvider", errutil.WithPublicMessage("Provider is invalid"))
|
||||||
ErrInvalidSettings = errutil.ValidationFailed("sso.settings", errutil.WithPublicMessage("Settings field is invalid"))
|
ErrInvalidSettings = errutil.ValidationFailed("sso.settings", errutil.WithPublicMessage("Settings field is invalid"))
|
||||||
|
@ -88,6 +88,10 @@ func (s *SSOSettingsService) GetForProvider(ctx context.Context, provider string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSOSettingsService) GetForProviderWithRedactedSecrets(ctx context.Context, provider string) (*models.SSOSettings, error) {
|
func (s *SSOSettingsService) GetForProviderWithRedactedSecrets(ctx context.Context, provider string) (*models.SSOSettings, error) {
|
||||||
|
if !s.isProviderConfigurable(provider) {
|
||||||
|
return nil, ssosettings.ErrNotConfigurable
|
||||||
|
}
|
||||||
|
|
||||||
storeSettings, err := s.GetForProvider(ctx, provider)
|
storeSettings, err := s.GetForProvider(ctx, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -136,7 +140,14 @@ func (s *SSOSettingsService) ListWithRedactedSecrets(ctx context.Context) ([]*mo
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, storeSetting := range storeSettings {
|
configurableSettings := make([]*models.SSOSettings, 0, len(s.cfg.SSOSettingsConfigurableProviders))
|
||||||
|
for _, provider := range storeSettings {
|
||||||
|
if s.isProviderConfigurable(provider.Provider) {
|
||||||
|
configurableSettings = append(configurableSettings, provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, storeSetting := range configurableSettings {
|
||||||
for k, v := range storeSetting.Settings {
|
for k, v := range storeSetting.Settings {
|
||||||
if strVal, ok := v.(string); ok {
|
if strVal, ok := v.(string); ok {
|
||||||
storeSetting.Settings[k] = setting.RedactedValue(k, strVal)
|
storeSetting.Settings[k] = setting.RedactedValue(k, strVal)
|
||||||
@ -144,12 +155,12 @@ func (s *SSOSettingsService) ListWithRedactedSecrets(ctx context.Context) ([]*mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return storeSettings, nil
|
return configurableSettings, 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) {
|
if !s.isProviderConfigurable(settings.Provider) {
|
||||||
return ssosettings.ErrInvalidProvider.Errorf("provider %s is not configurable", settings.Provider)
|
return ssosettings.ErrNotConfigurable
|
||||||
}
|
}
|
||||||
|
|
||||||
social, ok := s.reloadables[settings.Provider]
|
social, ok := s.reloadables[settings.Provider]
|
||||||
@ -195,6 +206,9 @@ func (s *SSOSettingsService) Patch(ctx context.Context, provider string, data ma
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSOSettingsService) Delete(ctx context.Context, provider string) error {
|
func (s *SSOSettingsService) Delete(ctx context.Context, provider string) error {
|
||||||
|
if !s.isProviderConfigurable(provider) {
|
||||||
|
return ssosettings.ErrNotConfigurable
|
||||||
|
}
|
||||||
return s.store.Delete(ctx, provider)
|
return s.store.Delete(ctx, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,6 +379,11 @@ func (s *SSOSettingsService) decryptSecrets(ctx context.Context, settings map[st
|
|||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SSOSettingsService) isProviderConfigurable(provider string) bool {
|
||||||
|
_, ok := s.cfg.SSOSettingsConfigurableProviders[provider]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// removeSecrets removes all the secrets from the map and replaces them with a redacted password
|
// removeSecrets removes all the secrets from the map and replaces them with a redacted password
|
||||||
// and returns a new map
|
// and returns a new map
|
||||||
func removeSecrets(settings map[string]any) map[string]any {
|
func removeSecrets(settings map[string]any) map[string]any {
|
||||||
@ -434,16 +453,6 @@ func isSecret(fieldName string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isProviderConfigurable(provider string) bool {
|
|
||||||
for _, configurable := range ssosettings.ConfigurableOAuthProviders {
|
|
||||||
if provider == configurable {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNewSecretValue(value string) bool {
|
func isNewSecretValue(value string) bool {
|
||||||
return value != setting.RedactedPassword
|
return value != setting.RedactedPassword
|
||||||
}
|
}
|
||||||
|
@ -588,16 +588,6 @@ func TestSSOSettingsService_ListWithRedactedSecrets(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Source: models.System,
|
Source: models.System,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Provider: "grafana_com",
|
|
||||||
Settings: map[string]any{
|
|
||||||
"enabled": true,
|
|
||||||
"secret": "*********",
|
|
||||||
"client_secret": "*********",
|
|
||||||
"client_id": "client_id",
|
|
||||||
},
|
|
||||||
Source: models.System,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@ -718,16 +708,6 @@ func TestSSOSettingsService_ListWithRedactedSecrets(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Source: models.System,
|
Source: models.System,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Provider: "grafana_com",
|
|
||||||
Settings: map[string]any{
|
|
||||||
"enabled": false,
|
|
||||||
"secret": "*********",
|
|
||||||
"client_secret": "*********",
|
|
||||||
"client_id": "client_id",
|
|
||||||
},
|
|
||||||
Source: models.System,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@ -1045,6 +1025,17 @@ func TestSSOSettingsService_Delete(t *testing.T) {
|
|||||||
require.ErrorIs(t, err, ssosettings.ErrNotFound)
|
require.ErrorIs(t, err, ssosettings.ErrNotFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should not delete the SSO settings if the provider is not configurable", func(t *testing.T) {
|
||||||
|
env := setupTestEnv(t)
|
||||||
|
env.cfg.SSOSettingsConfigurableProviders = map[string]bool{social.AzureADProviderName: true}
|
||||||
|
|
||||||
|
provider := social.GrafanaComProviderName
|
||||||
|
env.store.ExpectedError = nil
|
||||||
|
|
||||||
|
err := env.service.Delete(context.Background(), provider)
|
||||||
|
require.ErrorIs(t, err, ssosettings.ErrNotConfigurable)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("store fails to delete the SSO settings for the specified provider", func(t *testing.T) {
|
t.Run("store fails to delete the SSO settings for the specified provider", func(t *testing.T) {
|
||||||
env := setupTestEnv(t)
|
env := setupTestEnv(t)
|
||||||
|
|
||||||
@ -1214,8 +1205,20 @@ func setupTestEnv(t *testing.T) testEnv {
|
|||||||
|
|
||||||
fallbackStrategy.ExpectedIsMatch = true
|
fallbackStrategy.ExpectedIsMatch = true
|
||||||
|
|
||||||
|
cfg := &setting.Cfg{
|
||||||
|
SSOSettingsConfigurableProviders: map[string]bool{
|
||||||
|
"github": true,
|
||||||
|
"okta": true,
|
||||||
|
"azuread": true,
|
||||||
|
"google": true,
|
||||||
|
"generic_oauth": true,
|
||||||
|
"gitlab": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
svc := &SSOSettingsService{
|
svc := &SSOSettingsService{
|
||||||
logger: log.NewNopLogger(),
|
logger: log.NewNopLogger(),
|
||||||
|
cfg: cfg,
|
||||||
store: store,
|
store: store,
|
||||||
ac: accessControl,
|
ac: accessControl,
|
||||||
fbStrategies: []ssosettings.FallbackStrategy{fallbackStrategy},
|
fbStrategies: []ssosettings.FallbackStrategy{fallbackStrategy},
|
||||||
@ -1224,6 +1227,7 @@ func setupTestEnv(t *testing.T) testEnv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return testEnv{
|
return testEnv{
|
||||||
|
cfg: cfg,
|
||||||
service: svc,
|
service: svc,
|
||||||
store: store,
|
store: store,
|
||||||
ac: accessControl,
|
ac: accessControl,
|
||||||
@ -1234,6 +1238,7 @@ func setupTestEnv(t *testing.T) testEnv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testEnv struct {
|
type testEnv struct {
|
||||||
|
cfg *setting.Cfg
|
||||||
service *SSOSettingsService
|
service *SSOSettingsService
|
||||||
store *ssosettingstests.FakeStore
|
store *ssosettingstests.FakeStore
|
||||||
ac accesscontrol.AccessControl
|
ac accesscontrol.AccessControl
|
||||||
|
@ -287,7 +287,8 @@ type Cfg struct {
|
|||||||
ExtendedJWTExpectAudience string
|
ExtendedJWTExpectAudience string
|
||||||
|
|
||||||
// SSO Settings Auth
|
// SSO Settings Auth
|
||||||
SSOSettingsReloadInterval time.Duration
|
SSOSettingsReloadInterval time.Duration
|
||||||
|
SSOSettingsConfigurableProviders map[string]bool
|
||||||
|
|
||||||
// Dataproxy
|
// Dataproxy
|
||||||
SendUserHeader bool
|
SendUserHeader bool
|
||||||
@ -1628,7 +1629,12 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
|||||||
// SSO Settings
|
// SSO Settings
|
||||||
ssoSettings := iniFile.Section("sso_settings")
|
ssoSettings := iniFile.Section("sso_settings")
|
||||||
cfg.SSOSettingsReloadInterval = ssoSettings.Key("reload_interval").MustDuration(1 * time.Minute)
|
cfg.SSOSettingsReloadInterval = ssoSettings.Key("reload_interval").MustDuration(1 * time.Minute)
|
||||||
|
providers := ssoSettings.Key("configurable_providers").String()
|
||||||
|
|
||||||
|
cfg.SSOSettingsConfigurableProviders = make(map[string]bool)
|
||||||
|
for _, provider := range util.SplitString(providers) {
|
||||||
|
cfg.SSOSettingsConfigurableProviders[provider] = true
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,5 +2,5 @@ import { BASE_PATH } from '../constants';
|
|||||||
import { AuthProviderInfo } from '../types';
|
import { AuthProviderInfo } from '../types';
|
||||||
|
|
||||||
export function getProviderUrl(provider: AuthProviderInfo) {
|
export function getProviderUrl(provider: AuthProviderInfo) {
|
||||||
return BASE_PATH + (provider.configPath || `advanced/${provider.id}`);
|
return BASE_PATH + (provider.configPath || provider.id);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user