diff --git a/pkg/api/api.go b/pkg/api/api.go index 4f5cfa151b5..2ee56b8eb04 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -5,7 +5,6 @@ import ( "time" "github.com/go-macaron/binding" - "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/frontendlogging" @@ -15,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" ) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 37bb57b2c16..0f2e0fc934b 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -73,6 +73,7 @@ type HTTPServer struct { Bus bus.Bus `inject:""` RenderService rendering.Service `inject:""` Cfg *setting.Cfg `inject:""` + SettingsProvider setting.Provider `inject:""` HooksService *hooks.HooksService `inject:""` CacheService *localcache.CacheService `inject:""` DatasourceCache datasources.CacheService `inject:""` diff --git a/pkg/api/login.go b/pkg/api/login.go index 9580c8fc0d3..bf63b068c44 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -96,7 +96,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { } viewData.Settings["oauth"] = enabledOAuths - viewData.Settings["samlEnabled"] = hs.License.HasValidLicense() && hs.Cfg.SAMLEnabled + viewData.Settings["samlEnabled"] = hs.samlEnabled() if loginError, ok := tryGetEncryptedCookie(c, loginErrorCookieName); ok { // this cookie is only set whenever an OAuth login fails @@ -278,7 +278,7 @@ func (hs *HTTPServer) loginUserWithUser(user *models.User, c *models.ReqContext) } func (hs *HTTPServer) Logout(c *models.ReqContext) { - if hs.Cfg.SAMLEnabled && hs.Cfg.SAMLSingleLogoutEnabled && hs.License.HasValidLicense() { + if hs.samlSingleLogoutEnabled() { c.Redirect(hs.Cfg.AppSubURL + "/logout/saml") return } @@ -342,6 +342,14 @@ func (hs *HTTPServer) RedirectResponseWithError(ctx *models.ReqContext, err erro return response.Redirect(hs.Cfg.AppSubURL + "/login") } +func (hs *HTTPServer) samlEnabled() bool { + return hs.SettingsProvider.KeyValue("auth.saml", "enabled").MustBool(false) && hs.License.HasValidLicense() +} + +func (hs *HTTPServer) samlSingleLogoutEnabled() bool { + return hs.SettingsProvider.KeyValue("auth.saml", "single_logout").MustBool(false) && hs.samlEnabled() +} + func getLoginExternalError(err error) string { var createTokenErr *models.CreateTokenErr if errors.As(err, &createTokenErr) { diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index d1f9b4554d0..2d59cfb6783 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -94,8 +94,9 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) { sc := setupScenarioContext(t, "/login") cfg := setting.NewCfg() hs := &HTTPServer{ - Cfg: cfg, - License: &licensing.OSSLicensingService{}, + Cfg: cfg, + SettingsProvider: &setting.OSSImpl{Cfg: cfg}, + License: &licensing.OSSLicensingService{}, } sc.defaultHandler = routing.Wrap(func(w http.ResponseWriter, c *models.ReqContext) { @@ -154,9 +155,11 @@ func TestLoginViewRedirect(t *testing.T) { fakeViewIndex(t) sc := setupScenarioContext(t, "/login") + cfg := setting.NewCfg() hs := &HTTPServer{ - Cfg: setting.NewCfg(), - License: &licensing.OSSLicensingService{}, + Cfg: cfg, + SettingsProvider: &setting.OSSImpl{Cfg: cfg}, + License: &licensing.OSSLicensingService{}, } hs.Cfg.CookieSecure = true @@ -485,9 +488,11 @@ func TestLoginOAuthRedirect(t *testing.T) { fakeSetIndexViewData(t) sc := setupScenarioContext(t, "/login") + cfg := setting.NewCfg() hs := &HTTPServer{ - Cfg: setting.NewCfg(), - License: &licensing.OSSLicensingService{}, + Cfg: cfg, + SettingsProvider: &setting.OSSImpl{Cfg: cfg}, + License: &licensing.OSSLicensingService{}, } sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) { @@ -577,6 +582,7 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte sc.cfg.LoginCookieName = "grafana_session" hs := &HTTPServer{ Cfg: sc.cfg, + SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, License: &licensing.OSSLicensingService{}, AuthTokenService: auth.NewFakeUserAuthTokenService(), log: log.New("hello"), diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index 295f50fdc1d..67b7248a208 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/validations" + "github.com/grafana/grafana/pkg/setting" _ "github.com/grafana/loki/pkg/logproto" _ "github.com/grafana/loki/pkg/promtail/client" _ "github.com/grpc-ecosystem/go-grpc-middleware" @@ -31,6 +32,7 @@ func init() { registry.RegisterService(&licensing.OSSLicensingService{}) registry.RegisterService(&validations.OSSPluginRequestValidator{}) registry.RegisterService(&ossaccesscontrol.OSSAccessControlService{}) + registry.RegisterService(&setting.OSSImpl{}) } var IsEnterprise bool = false diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go new file mode 100644 index 00000000000..cddc8177981 --- /dev/null +++ b/pkg/setting/provider.go @@ -0,0 +1,141 @@ +package setting + +import ( + "errors" + "strings" + "time" + + "gopkg.in/ini.v1" +) + +var ( + ErrOperationNotPermitted = errors.New("operation not permitted") +) + +type ValidationError struct { + Errors []error +} + +func (v ValidationError) Error() string { + builder := strings.Builder{} + + for i, e := range v.Errors { + builder.WriteString(e.Error()) + if i != len(v.Errors)-1 { + builder.WriteString(", ") + } + } + + return builder.String() +} + +// Provider is a settings provider abstraction +// with thread-safety and runtime updates. +type Provider interface { + // Update + Update(updates SettingsBag, removals SettingsRemovals) error + // KeyValue returns a key-value abstraction + // for the given pair of section and key. + KeyValue(section, key string) KeyValue + // Section returns a settings section + // abstraction for the given section name. + Section(section string) Section + // RegisterReloadHandler registers a handler for validation and reload + // of configuration updates tied to a specific section + RegisterReloadHandler(section string, handler ReloadHandler) +} + +// Section is a settings section copy +// with all of its pairs of keys-values. +type Section interface { + // KeyValue returns a key-value + // abstraction for the given key. + KeyValue(key string) KeyValue +} + +// KeyValue represents a settings key-value +// for a given pair of section and key. +type KeyValue interface { + // Key returns pair's key. + Key() string + // Value returns pair's value. + Value() string + + // MustString returns the value's string representation + // If empty, then it returns the given default. + MustString(defaultVal string) string + // MustBool returns the value's boolean representation + // Otherwise returns the given default. + MustBool(defaultVal bool) bool + // MustDuration returns the value's time.Duration + // representation. Otherwise returns the given default. + MustDuration(defaultVal time.Duration) time.Duration +} + +// ReloadHandler defines the expected behaviour from a +// service that have support for configuration reloads. +type ReloadHandler interface { + // Reload handles reloading of configuration changes. + Reload(section Section) error + + // Validate validates the configuration, if the validation + // fails the configuration will not be updated neither reloaded. + Validate(section Section) error +} + +type SettingsBag map[string]map[string]string +type SettingsRemovals map[string][]string + +type OSSImpl struct { + Cfg *Cfg `inject:""` +} + +func (o OSSImpl) Init() error { + return nil +} + +func (OSSImpl) Update(SettingsBag, SettingsRemovals) error { + return errors.New("oss settings provider do not have support for settings updates") +} + +func (o *OSSImpl) KeyValue(section, key string) KeyValue { + return o.Section(section).KeyValue(key) +} + +func (o *OSSImpl) Section(section string) Section { + return §ionImpl{section: o.Cfg.Raw.Section(section)} +} + +func (OSSImpl) RegisterReloadHandler(string, ReloadHandler) {} + +type keyValImpl struct { + key *ini.Key +} + +func (k *keyValImpl) Key() string { + return k.key.Name() +} + +func (k *keyValImpl) Value() string { + return k.key.Value() +} + +func (k *keyValImpl) MustString(defaultVal string) string { + return k.key.MustString(defaultVal) +} + +func (k *keyValImpl) MustBool(defaultVal bool) bool { + return k.key.MustBool(defaultVal) +} + +func (k *keyValImpl) MustDuration(defaultVal time.Duration) time.Duration { + return k.key.MustDuration(defaultVal) +} + +type sectionImpl struct { + section *ini.Section +} + +func (s *sectionImpl) KeyValue(key string) KeyValue { + return &keyValImpl{s.section.Key(key)} +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 9a06ca0ca95..474a12562e4 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -297,10 +297,6 @@ type Cfg struct { // OAuth OAuthCookieMaxAge int - // SAML Auth - SAMLEnabled bool - SAMLSingleLogoutEnabled bool - // JWT Auth JWTAuthEnabled bool JWTAuthHeaderName string @@ -1175,10 +1171,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false) cfg.SigV4AuthEnabled = SigV4AuthEnabled - // SAML auth - cfg.SAMLEnabled = iniFile.Section("auth.saml").Key("enabled").MustBool(false) - cfg.SAMLSingleLogoutEnabled = iniFile.Section("auth.saml").Key("single_logout").MustBool(false) - // anonymous access AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) cfg.AnonymousEnabled = AnonymousEnabled