mirror of
https://github.com/grafana/grafana.git
synced 2024-12-24 16:10:22 -06:00
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:
parent
c4f906f7fa
commit
50a635bc7e
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user