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
This commit is contained in:
Misi 2024-10-16 16:27:44 +02:00 committed by GitHub
parent c4f906f7fa
commit 50a635bc7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 156 additions and 39 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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()

View File

@ -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
}

View File

@ -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)

View File

@ -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"

View File

@ -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) {

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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"