Auth: Introduce authn.SSOClientConfig to get client config from SSOSettings service (#94618)

* wip

* possible solution

* Separate interface for SSO settings clients

* Rename interface

* Fix tests

* Rename

* Change GetClientConfig to comma ok idiom
pull/94834/head
Misi 8 months ago committed by GitHub
parent c4f906f7fa
commit 50a635bc7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 18
      pkg/api/login.go
  2. 6
      pkg/api/login_test.go
  3. 17
      pkg/login/social/social.go
  4. 6
      pkg/services/anonymous/anonimpl/client.go
  5. 17
      pkg/services/authn/authn.go
  6. 14
      pkg/services/authn/authnimpl/service.go
  7. 38
      pkg/services/authn/authntest/fake.go
  8. 24
      pkg/services/authn/authntest/mock.go
  9. 8
      pkg/services/authn/clients/api_key.go
  10. 3
      pkg/services/authn/clients/ext_jwt.go
  11. 4
      pkg/services/authn/clients/form.go
  12. 4
      pkg/services/authn/clients/jwt.go
  13. 20
      pkg/services/authn/clients/oauth.go
  14. 4
      pkg/services/authn/clients/render.go

@ -359,15 +359,27 @@ func (hs *HTTPServer) samlEnabled() bool {
} }
func (hs *HTTPServer) samlName() string { 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 { 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 { 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 { func getLoginExternalError(err error) string {

@ -659,7 +659,11 @@ func TestLogoutSaml(t *testing.T) {
license.On("FeatureEnabled", "saml").Return(true) license.On("FeatureEnabled", "saml").Return(true)
hs := &HTTPServer{ hs := &HTTPServer{
authnService: &authntest.FakeService{}, authnService: &authntest.FakeService{
ExpectedClientConfig: &authntest.FakeSSOClientConfig{
ExpectedIsSingleLogoutEnabled: true,
},
},
Cfg: sc.cfg, Cfg: sc.cfg,
SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg},
License: license, License: license,

@ -27,9 +27,7 @@ const (
LDAPProviderName = "ldap" LDAPProviderName = "ldap"
) )
var ( var SocialBaseUrl = "/login/"
SocialBaseUrl = "/login/"
)
type Service interface { type Service interface {
GetOAuthProviders() map[string]bool 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 { type BasicUserInfo struct {
Id string Id string
Name string Name string

@ -22,8 +22,10 @@ var (
errDeviceLimit = errutil.Unauthorized("anonymous.device-limit-reached", errutil.WithPublicMessage("Anonymous device limit reached. Contact Administrator")) errDeviceLimit = errutil.Unauthorized("anonymous.device-limit-reached", errutil.WithPublicMessage("Anonymous device limit reached. Contact Administrator"))
) )
var _ authn.ContextAwareClient = new(Anonymous) var (
var _ authn.IdentityResolverClient = new(Anonymous) _ authn.ContextAwareClient = new(Anonymous)
_ authn.IdentityResolverClient = new(Anonymous)
)
type Anonymous struct { type Anonymous struct {
cfg *setting.Cfg cfg *setting.Cfg

@ -86,6 +86,15 @@ type Authenticator interface {
Authenticate(ctx context.Context, r *Request) (*Identity, error) 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 { type Service interface {
Authenticator Authenticator
// RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication. // 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" // - "saml" = "auth.client.saml"
// - "github" = "auth.client.github" // - "github" = "auth.client.github"
IsClientEnabled(client string) bool 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 { type IdentitySynchronizer interface {
@ -168,6 +180,11 @@ type LogoutClient interface {
Logout(ctx context.Context, user identity.Requester) (*Redirect, bool) Logout(ctx context.Context, user identity.Requester) (*Redirect, bool)
} }
type SSOSettingsAwareClient interface {
Client
GetConfig() SSOClientConfig
}
type PasswordClient interface { type PasswordClient interface {
AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error) AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error)
} }

@ -380,6 +380,20 @@ func (s *Service) IsClientEnabled(name string) bool {
return client.IsEnabled() 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 { func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error {
ctx, span := s.tracer.Start(ctx, "authn.SyncIdentity") ctx, span := s.tracer.Start(ctx, "authn.SyncIdentity")
defer span.End() defer span.End()

@ -8,10 +8,33 @@ import (
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
) )
var _ authn.Service = new(FakeService) var _ authn.SSOClientConfig = new(FakeSSOClientConfig)
var _ authn.IdentitySynchronizer = new(FakeService)
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 { type FakeService struct {
ExpectedClientConfig authn.SSOClientConfig
ExpectedErr error ExpectedErr error
ExpectedRedirect *authn.Redirect ExpectedRedirect *authn.Redirect
ExpectedIdentity *authn.Identity ExpectedIdentity *authn.Identity
@ -44,6 +67,13 @@ func (f *FakeService) IsClientEnabled(name string) bool {
return true 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) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) {}
func (f *FakeService) RegisterPreLogoutHook(hook authn.PreLogoutHookFn, 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) IsEnabled() bool { return true }
func (f *FakeClient) GetConfig() authn.SSOClientConfig {
return nil
}
func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool { func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool {
return f.ExpectedTest return f.ExpectedTest
} }

@ -9,8 +9,10 @@ import (
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
) )
var _ authn.Service = new(MockService) var (
var _ authn.IdentitySynchronizer = new(MockService) _ authn.Service = new(MockService)
_ authn.IdentitySynchronizer = new(MockService)
)
type MockService struct { type MockService struct {
SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error SyncIdentityFunc func(ctx context.Context, identity *authn.Identity) error
@ -25,6 +27,10 @@ func (m *MockService) IsClientEnabled(name string) bool {
panic("unimplemented") 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) { func (m *MockService) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
panic("unimplemented") panic("unimplemented")
} }
@ -66,10 +72,12 @@ func (m *MockService) SyncIdentity(ctx context.Context, identity *authn.Identity
return nil return nil
} }
var _ authn.HookClient = new(MockClient) var (
var _ authn.LogoutClient = new(MockClient) _ authn.HookClient = new(MockClient)
var _ authn.ContextAwareClient = new(MockClient) _ authn.LogoutClient = new(MockClient)
var _ authn.IdentityResolverClient = new(MockClient) _ authn.ContextAwareClient = new(MockClient)
_ authn.IdentityResolverClient = new(MockClient)
)
type MockClient struct { type MockClient struct {
NameFunc func() string NameFunc func() string
@ -100,6 +108,10 @@ func (m MockClient) IsEnabled() bool {
return true return true
} }
func (m MockClient) GetConfig() authn.SSOClientConfig {
return nil
}
func (m MockClient) Test(ctx context.Context, r *authn.Request) bool { func (m MockClient) Test(ctx context.Context, r *authn.Request) bool {
if m.TestFunc != nil { if m.TestFunc != nil {
return m.TestFunc(ctx, r) return m.TestFunc(ctx, r)

@ -27,9 +27,11 @@ var (
errAPIKeyOrgMismatch = errutil.Unauthorized("api-key.organization-mismatch", errutil.WithPublicMessage("API key does not belong to the requested organization")) errAPIKeyOrgMismatch = errutil.Unauthorized("api-key.organization-mismatch", errutil.WithPublicMessage("API key does not belong to the requested organization"))
) )
var _ authn.HookClient = new(APIKey) var (
var _ authn.ContextAwareClient = new(APIKey) _ authn.HookClient = new(APIKey)
var _ authn.IdentityResolverClient = new(APIKey) _ authn.ContextAwareClient = new(APIKey)
_ authn.IdentityResolverClient = new(APIKey)
)
const ( const (
metaKeyID = "keyID" metaKeyID = "keyID"

@ -150,7 +150,8 @@ func (s *ExtendedJWT) authenticateAsUser(
RestrictedActions: accessTokenClaims.Rest.DelegatedPermissions, RestrictedActions: accessTokenClaims.Rest.DelegatedPermissions,
}, },
FetchSyncedUser: true, FetchSyncedUser: true,
}}, nil },
}, nil
} }
func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) { func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {

@ -8,9 +8,7 @@ import (
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
var ( var errBadForm = errutil.BadRequest("form-auth.invalid", errutil.WithPublicMessage("bad login data"))
errBadForm = errutil.BadRequest("form-auth.invalid", errutil.WithPublicMessage("bad login data"))
)
var _ authn.Client = new(Form) var _ authn.Client = new(Form)

@ -73,7 +73,8 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
SyncOrgRoles: !s.cfg.JWTAuth.SkipOrgRoleSync, SyncOrgRoles: !s.cfg.JWTAuth.SkipOrgRoleSync,
AllowSignUp: s.cfg.JWTAuth.AutoSignUp, AllowSignUp: s.cfg.JWTAuth.AutoSignUp,
SyncTeams: s.cfg.JWTAuth.GroupsAttributePath != "", SyncTeams: s.cfg.JWTAuth.GroupsAttributePath != "",
}} },
}
if key := s.cfg.JWTAuth.UsernameClaim; key != "" { if key := s.cfg.JWTAuth.UsernameClaim; key != "" {
id.Login, _ = claims[key].(string) 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 return role, &grafanaAdmin, nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -60,8 +60,11 @@ func fromSocialErr(err *connectors.SocialError) error {
return errutil.Unauthorized("auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err) return errutil.Unauthorized("auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err)
} }
var _ authn.LogoutClient = new(OAuth) var (
var _ authn.RedirectClient = new(OAuth) _ authn.LogoutClient = new(OAuth)
_ authn.RedirectClient = new(OAuth)
_ authn.SSOSettingsAwareClient = new(OAuth)
)
func ProvideOAuth( func ProvideOAuth(
name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService, name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService,
@ -203,6 +206,15 @@ func (c *OAuth) IsEnabled() bool {
return provider.Enabled 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) { func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) {
var opts []oauth2.AuthCodeOption var opts []oauth2.AuthCodeOption
@ -274,7 +286,7 @@ func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Red
return nil, false 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 { if idToken, ok := token.Extra("id_token").(string); ok {
redirectURL = withIDTokenHint(redirectURL, idToken) redirectURL = withIDTokenHint(redirectURL, idToken)
} }
@ -346,7 +358,7 @@ func withIDTokenHint(redirectURL string, idToken string) string {
return u.String() return u.String()
} }
func isOICDLogout(redirectUrl string) bool { func isOIDCLogout(redirectUrl string) bool {
if redirectUrl == "" { if redirectUrl == "" {
return false return false
} }

@ -13,9 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
) )
var ( var errInvalidRenderKey = errutil.Unauthorized("render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key"))
errInvalidRenderKey = errutil.Unauthorized("render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key"))
)
const ( const (
renderCookieName = "renderKey" renderCookieName = "renderKey"

Loading…
Cancel
Save