diff --git a/pkg/api/login.go b/pkg/api/login.go index dfca323c83f..338ccf5a24e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -359,15 +359,27 @@ func (hs *HTTPServer) samlEnabled() bool { } func (hs *HTTPServer) samlName() string { - return hs.SettingsProvider.KeyValue("auth.saml", "name").MustString("SAML") + config, ok := hs.authnService.GetClientConfig(authn.ClientSAML) + if !ok { + return "" + } + return config.GetDisplayName() } func (hs *HTTPServer) samlSingleLogoutEnabled() bool { - return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "single_logout").MustBool(false) && hs.samlEnabled() + config, ok := hs.authnService.GetClientConfig(authn.ClientSAML) + if !ok { + return false + } + return hs.samlEnabled() && config.IsSingleLogoutEnabled() } func (hs *HTTPServer) samlAutoLoginEnabled() bool { - return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "auto_login").MustBool(false) + config, ok := hs.authnService.GetClientConfig(authn.ClientSAML) + if !ok { + return false + } + return hs.samlEnabled() && config.IsAutoLoginEnabled() } func getLoginExternalError(err error) string { diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 4e4fbcad787..c6dad435ae0 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -659,7 +659,11 @@ func TestLogoutSaml(t *testing.T) { license.On("FeatureEnabled", "saml").Return(true) hs := &HTTPServer{ - authnService: &authntest.FakeService{}, + authnService: &authntest.FakeService{ + ExpectedClientConfig: &authntest.FakeSSOClientConfig{ + ExpectedIsSingleLogoutEnabled: true, + }, + }, Cfg: sc.cfg, SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, License: license, diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 889622c4494..12cf2dfd0a7 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -27,9 +27,7 @@ const ( LDAPProviderName = "ldap" ) -var ( - SocialBaseUrl = "/login/" -) +var SocialBaseUrl = "/login/" type Service interface { GetOAuthProviders() map[string]bool @@ -101,6 +99,19 @@ func NewOAuthInfo() *OAuthInfo { } } +func (o *OAuthInfo) GetDisplayName() string { + return o.Name +} + +func (o *OAuthInfo) IsSingleLogoutEnabled() bool { + // OIDC SLO is not supported + return false +} + +func (o *OAuthInfo) IsAutoLoginEnabled() bool { + return o.AutoLogin +} + type BasicUserInfo struct { Id string Name string diff --git a/pkg/services/anonymous/anonimpl/client.go b/pkg/services/anonymous/anonimpl/client.go index 43c1d8107c2..b2d0ca6ddce 100644 --- a/pkg/services/anonymous/anonimpl/client.go +++ b/pkg/services/anonymous/anonimpl/client.go @@ -22,8 +22,10 @@ var ( errDeviceLimit = errutil.Unauthorized("anonymous.device-limit-reached", errutil.WithPublicMessage("Anonymous device limit reached. Contact Administrator")) ) -var _ authn.ContextAwareClient = new(Anonymous) -var _ authn.IdentityResolverClient = new(Anonymous) +var ( + _ authn.ContextAwareClient = new(Anonymous) + _ authn.IdentityResolverClient = new(Anonymous) +) type Anonymous struct { cfg *setting.Cfg diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index a024beaf2fb..a689fdfa0f7 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -86,6 +86,15 @@ type Authenticator interface { Authenticate(ctx context.Context, r *Request) (*Identity, error) } +type SSOClientConfig interface { + // GetDisplayName returns the display name of the client + GetDisplayName() string + // IsAutoLoginEnabled returns true if the client has auto login enabled + IsAutoLoginEnabled() bool + // IsSingleLogoutEnabled returns true if the client has single logout enabled + IsSingleLogoutEnabled() bool +} + type Service interface { Authenticator // RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication. @@ -120,6 +129,9 @@ type Service interface { // - "saml" = "auth.client.saml" // - "github" = "auth.client.github" IsClientEnabled(client string) bool + + // GetClientConfig returns the client configuration for the given client and a boolean indicating if the config was present. + GetClientConfig(client string) (SSOClientConfig, bool) } type IdentitySynchronizer interface { @@ -168,6 +180,11 @@ type LogoutClient interface { Logout(ctx context.Context, user identity.Requester) (*Redirect, bool) } +type SSOSettingsAwareClient interface { + Client + GetConfig() SSOClientConfig +} + type PasswordClient interface { AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error) } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 644adebc386..4b24f92063b 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -380,6 +380,20 @@ func (s *Service) IsClientEnabled(name string) bool { return client.IsEnabled() } +func (s *Service) GetClientConfig(name string) (authn.SSOClientConfig, bool) { + client, ok := s.clients[name] + if !ok { + return nil, false + } + + ssoSettingsAwareClient, ok := client.(authn.SSOSettingsAwareClient) + if !ok { + return nil, false + } + + return ssoSettingsAwareClient.GetConfig(), true +} + func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error { ctx, span := s.tracer.Start(ctx, "authn.SyncIdentity") defer span.End() diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index 91fc6473032..60892f99bf0 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -8,16 +8,39 @@ import ( "github.com/grafana/grafana/pkg/services/authn" ) -var _ authn.Service = new(FakeService) -var _ authn.IdentitySynchronizer = new(FakeService) +var _ authn.SSOClientConfig = new(FakeSSOClientConfig) + +type FakeSSOClientConfig struct { + ExpectedName string + ExpectedIsAutoLoginEnabled bool + ExpectedIsSingleLogoutEnabled bool +} + +func (f *FakeSSOClientConfig) GetDisplayName() string { + return f.ExpectedName +} + +func (f *FakeSSOClientConfig) IsAutoLoginEnabled() bool { + return f.ExpectedIsAutoLoginEnabled +} + +func (f *FakeSSOClientConfig) IsSingleLogoutEnabled() bool { + return f.ExpectedIsSingleLogoutEnabled +} + +var ( + _ authn.Service = new(FakeService) + _ authn.IdentitySynchronizer = new(FakeService) +) type FakeService struct { - ExpectedErr error - ExpectedRedirect *authn.Redirect - ExpectedIdentity *authn.Identity - ExpectedErrs []error - ExpectedIdentities []*authn.Identity - CurrentIndex int + ExpectedClientConfig authn.SSOClientConfig + ExpectedErr error + ExpectedRedirect *authn.Redirect + ExpectedIdentity *authn.Identity + ExpectedErrs []error + ExpectedIdentities []*authn.Identity + CurrentIndex int } func (f *FakeService) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -44,6 +67,13 @@ func (f *FakeService) IsClientEnabled(name string) bool { return true } +func (f *FakeService) GetClientConfig(name string) (authn.SSOClientConfig, bool) { + if f.ExpectedClientConfig == nil { + return nil, false + } + return f.ExpectedClientConfig, true +} + func (f *FakeService) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) {} func (f *FakeService) RegisterPreLogoutHook(hook authn.PreLogoutHookFn, priority uint) {} @@ -127,6 +157,10 @@ func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn func (f FakeClient) IsEnabled() bool { return true } +func (f *FakeClient) GetConfig() authn.SSOClientConfig { + return nil +} + func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool { return f.ExpectedTest } diff --git a/pkg/services/authn/authntest/mock.go b/pkg/services/authn/authntest/mock.go index f9422291f1d..e586d2ac8de 100644 --- a/pkg/services/authn/authntest/mock.go +++ b/pkg/services/authn/authntest/mock.go @@ -9,8 +9,10 @@ import ( "github.com/grafana/grafana/pkg/services/authn" ) -var _ authn.Service = new(MockService) -var _ authn.IdentitySynchronizer = new(MockService) +var ( + _ authn.Service = new(MockService) + _ authn.IdentitySynchronizer = new(MockService) +) type MockService struct { SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error @@ -25,6 +27,10 @@ func (m *MockService) IsClientEnabled(name string) bool { panic("unimplemented") } +func (m *MockService) GetClientConfig(name string) (authn.SSOClientConfig, bool) { + panic("unimplemented") +} + func (m *MockService) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) { panic("unimplemented") } @@ -66,10 +72,12 @@ func (m *MockService) SyncIdentity(ctx context.Context, identity *authn.Identity return nil } -var _ authn.HookClient = new(MockClient) -var _ authn.LogoutClient = new(MockClient) -var _ authn.ContextAwareClient = new(MockClient) -var _ authn.IdentityResolverClient = new(MockClient) +var ( + _ authn.HookClient = new(MockClient) + _ authn.LogoutClient = new(MockClient) + _ authn.ContextAwareClient = new(MockClient) + _ authn.IdentityResolverClient = new(MockClient) +) type MockClient struct { NameFunc func() string @@ -100,6 +108,10 @@ func (m MockClient) IsEnabled() bool { return true } +func (m MockClient) GetConfig() authn.SSOClientConfig { + return nil +} + func (m MockClient) Test(ctx context.Context, r *authn.Request) bool { if m.TestFunc != nil { return m.TestFunc(ctx, r) diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 26f3f884174..f53e8862bf3 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -27,9 +27,11 @@ var ( errAPIKeyOrgMismatch = errutil.Unauthorized("api-key.organization-mismatch", errutil.WithPublicMessage("API key does not belong to the requested organization")) ) -var _ authn.HookClient = new(APIKey) -var _ authn.ContextAwareClient = new(APIKey) -var _ authn.IdentityResolverClient = new(APIKey) +var ( + _ authn.HookClient = new(APIKey) + _ authn.ContextAwareClient = new(APIKey) + _ authn.IdentityResolverClient = new(APIKey) +) const ( metaKeyID = "keyID" diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 638101615a2..8c9c33b0e4e 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -150,7 +150,8 @@ func (s *ExtendedJWT) authenticateAsUser( RestrictedActions: accessTokenClaims.Rest.DelegatedPermissions, }, FetchSyncedUser: true, - }}, nil + }, + }, nil } func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) { diff --git a/pkg/services/authn/clients/form.go b/pkg/services/authn/clients/form.go index af9fb33fb23..af701b983d4 100644 --- a/pkg/services/authn/clients/form.go +++ b/pkg/services/authn/clients/form.go @@ -8,9 +8,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -var ( - errBadForm = errutil.BadRequest("form-auth.invalid", errutil.WithPublicMessage("bad login data")) -) +var errBadForm = errutil.BadRequest("form-auth.invalid", errutil.WithPublicMessage("bad login data")) var _ authn.Client = new(Form) diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 430985d7ae1..edc1f43dbc9 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -73,7 +73,8 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi SyncOrgRoles: !s.cfg.JWTAuth.SkipOrgRoleSync, AllowSignUp: s.cfg.JWTAuth.AutoSignUp, SyncTeams: s.cfg.JWTAuth.GroupsAttributePath != "", - }} + }, + } if key := s.cfg.JWTAuth.UsernameClaim; key != "" { id.Login, _ = claims[key].(string) @@ -117,7 +118,6 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi return role, &grafanaAdmin, nil }) - if err != nil { return nil, err } diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 48ae37c88c1..3c09fc46ee2 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -60,8 +60,11 @@ func fromSocialErr(err *connectors.SocialError) error { return errutil.Unauthorized("auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err) } -var _ authn.LogoutClient = new(OAuth) -var _ authn.RedirectClient = new(OAuth) +var ( + _ authn.LogoutClient = new(OAuth) + _ authn.RedirectClient = new(OAuth) + _ authn.SSOSettingsAwareClient = new(OAuth) +) func ProvideOAuth( name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService, @@ -203,6 +206,15 @@ func (c *OAuth) IsEnabled() bool { return provider.Enabled } +func (c *OAuth) GetConfig() authn.SSOClientConfig { + provider := c.socialService.GetOAuthInfoProvider(c.providerName) + if provider == nil { + return nil + } + + return provider +} + func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) { var opts []oauth2.AuthCodeOption @@ -274,7 +286,7 @@ func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Red return nil, false } - if isOICDLogout(redirectURL) && token != nil && token.Valid() { + if isOIDCLogout(redirectURL) && token != nil && token.Valid() { if idToken, ok := token.Extra("id_token").(string); ok { redirectURL = withIDTokenHint(redirectURL, idToken) } @@ -346,7 +358,7 @@ func withIDTokenHint(redirectURL string, idToken string) string { return u.String() } -func isOICDLogout(redirectUrl string) bool { +func isOIDCLogout(redirectUrl string) bool { if redirectUrl == "" { return false } diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index e63507ff710..7f60ef6ecf9 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -13,9 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/rendering" ) -var ( - errInvalidRenderKey = errutil.Unauthorized("render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key")) -) +var errInvalidRenderKey = errutil.Unauthorized("render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key")) const ( renderCookieName = "renderKey"