Auth: Add IsClientEnabled and IsEnabled for the authn.Service and authn.Client interfaces (#86034)

* Add `Service. IsClientEnabled` and `Client.IsEnabled` functions

* Implement `IsEnabled` function for authn clients

* Implement `IsClientEnabled` function for authn services
This commit is contained in:
linoman
2024-04-15 02:54:50 -06:00
committed by GitHub
parent 2b62167842
commit 51da96d94e
19 changed files with 140 additions and 5 deletions

View File

@@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
@@ -58,9 +59,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
} }
hs := &HTTPServer{ hs := &HTTPServer{
Cfg: cfg, authnService: &authntest.FakeService{},
Features: features, Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg}, Features: features,
License: &licensing.OSSLicensingService{Cfg: cfg},
RenderService: &rendering.RenderingService{ RenderService: &rendering.RenderingService{
Cfg: cfg, Cfg: cfg,
RendererPluginManager: &fakeRendererPluginManager{}, RendererPluginManager: &fakeRendererPluginManager{},

View File

@@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
@@ -329,7 +328,7 @@ func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err
} }
func (hs *HTTPServer) samlEnabled() bool { func (hs *HTTPServer) samlEnabled() bool {
return hs.SettingsProvider.KeyValue("auth.saml", "enabled").MustBool(false) && hs.License.FeatureEnabled(social.SAMLProviderName) return hs.authnService.IsClientEnabled(authn.ClientSAML)
} }
func (hs *HTTPServer) samlName() string { func (hs *HTTPServer) samlName() string {

View File

@@ -491,6 +491,7 @@ func TestLoginOAuthRedirect(t *testing.T) {
oAuthInfos: oAuthInfos, oAuthInfos: oAuthInfos,
} }
hs := &HTTPServer{ hs := &HTTPServer{
authnService: &authntest.FakeService{},
Cfg: cfg, Cfg: cfg,
SettingsProvider: &setting.OSSImpl{Cfg: cfg}, SettingsProvider: &setting.OSSImpl{Cfg: cfg},
License: &licensing.OSSLicensingService{}, License: &licensing.OSSLicensingService{},
@@ -657,6 +658,7 @@ func TestLogoutSaml(t *testing.T) {
license.On("FeatureEnabled", "saml").Return(true) license.On("FeatureEnabled", "saml").Return(true)
hs := &HTTPServer{ hs := &HTTPServer{
authnService: &authntest.FakeService{},
Cfg: sc.cfg, Cfg: sc.cfg,
SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg},
License: license, License: license,

View File

@@ -58,6 +58,10 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.
}, nil }, nil
} }
func (a *Anonymous) IsEnabled() bool {
return a.cfg.AnonymousEnabled
}
func (a *Anonymous) Test(ctx context.Context, r *authn.Request) bool { func (a *Anonymous) Test(ctx context.Context, r *authn.Request) bool {
// If anonymous client is register it can always be used for authentication // If anonymous client is register it can always be used for authentication
return true return true

View File

@@ -94,6 +94,19 @@ type Service interface {
// RegisterClient will register a new authn.Client that can be used for authentication // RegisterClient will register a new authn.Client that can be used for authentication
RegisterClient(c Client) RegisterClient(c Client)
// IsClientEnabled returns true if the client is enabled.
//
// The client lookup follows the same formats used by the `authn` package
// constants.
//
// For OAuth clients, use the `authn.ClientWithPrefix(name)` to get the provider
// name. Append the prefix `auth.client.{providerName}`.
//
// Example:
// - "saml" = "auth.client.saml"
// - "github" = "auth.client.github"
IsClientEnabled(client string) bool
} }
type IdentitySynchronizer interface { type IdentitySynchronizer interface {
@@ -105,6 +118,8 @@ type Client interface {
Name() string Name() string
// Authenticate performs the authentication for the request // Authenticate performs the authentication for the request
Authenticate(ctx context.Context, r *Request) (*Identity, error) Authenticate(ctx context.Context, r *Request) (*Identity, error)
// IsEnabled returns the enabled status of the client
IsEnabled() bool
} }
// ContextAwareClient is an optional interface that auth client can implement. // ContextAwareClient is an optional interface that auth client can implement.

View File

@@ -320,6 +320,15 @@ func (s *Service) RegisterClient(c authn.Client) {
} }
} }
func (s *Service) IsClientEnabled(name string) bool {
client, ok := s.clients[name]
if !ok {
return false
}
return client.IsEnabled()
}
func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error { func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error {
r := &authn.Request{OrgID: identity.OrgID} r := &authn.Request{OrgID: identity.OrgID}
// hack to not update last seen on external syncs // hack to not update last seen on external syncs

View File

@@ -40,6 +40,10 @@ func (f *FakeService) Authenticate(ctx context.Context, r *authn.Request) (*auth
return f.ExpectedIdentity, f.ExpectedErr return f.ExpectedIdentity, f.ExpectedErr
} }
func (f *FakeService) IsClientEnabled(name string) bool {
return true
}
func (f *FakeService) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) {} func (f *FakeService) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) {}
func (f *FakeService) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) { func (f *FakeService) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
@@ -119,6 +123,8 @@ func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn
return f.ExpectedIdentity, f.ExpectedErr return f.ExpectedIdentity, f.ExpectedErr
} }
func (f FakeClient) IsEnabled() bool { return true }
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
} }
@@ -161,6 +167,8 @@ func (f FakeRedirectClient) Authenticate(ctx context.Context, r *authn.Request)
return f.ExpectedIdentity, f.ExpectedErr return f.ExpectedIdentity, f.ExpectedErr
} }
func (f FakeRedirectClient) IsEnabled() bool { return true }
func (f FakeRedirectClient) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) { func (f FakeRedirectClient) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) {
return f.ExpectedRedirect, f.ExpectedErr return f.ExpectedRedirect, f.ExpectedErr
} }

View File

@@ -20,6 +20,10 @@ func (m *MockService) Authenticate(ctx context.Context, r *authn.Request) (*auth
panic("unimplemented") panic("unimplemented")
} }
func (m *MockService) IsClientEnabled(name string) 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")
} }
@@ -87,6 +91,10 @@ func (m MockClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.
return nil, nil return nil, nil
} }
func (m MockClient) IsEnabled() bool {
return true
}
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)

View File

@@ -70,6 +70,10 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
return newServiceAccountIdentity(key), nil return newServiceAccountIdentity(key), nil
} }
func (s *APIKey) IsEnabled() bool {
return true
}
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) { func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
fn := s.getFromToken fn := s.getFromToken
if !strings.HasPrefix(token, satokengen.GrafanaPrefix) { if !strings.HasPrefix(token, satokengen.GrafanaPrefix) {

View File

@@ -38,6 +38,10 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return c.client.AuthenticatePassword(ctx, r, username, password) return c.client.AuthenticatePassword(ctx, r, username, password)
} }
func (c *Basic) IsEnabled() bool {
return true
}
func (c *Basic) Test(ctx context.Context, r *authn.Request) bool { func (c *Basic) Test(ctx context.Context, r *authn.Request) bool {
if r.HTTPRequest == nil { if r.HTTPRequest == nil {
return false return false

View File

@@ -91,6 +91,10 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth
return s.authenticateAsService(claims) return s.authenticateAsService(claims)
} }
func (s *ExtendedJWT) IsEnabled() bool {
return s.cfg.ExtJWTAuth.Enabled
}
func (s *ExtendedJWT) authenticateAsUser(idTokenClaims, func (s *ExtendedJWT) authenticateAsUser(idTokenClaims,
accessTokenClaims *ExtendedJWTClaims) (*authn.Identity, error) { accessTokenClaims *ExtendedJWTClaims) (*authn.Identity, error) {
// Only allow access policies to impersonate // Only allow access policies to impersonate

View File

@@ -38,3 +38,7 @@ func (c *Form) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ident
} }
return c.client.AuthenticatePassword(ctx, r, form.Username, form.Password) return c.client.AuthenticatePassword(ctx, r, form.Username, form.Password)
} }
func (c *Form) IsEnabled() bool {
return true
}

View File

@@ -20,6 +20,10 @@ func (i *IdentityClient) Name() string {
return "identity" return "identity"
} }
func (i *IdentityClient) IsEnabled() bool {
return true
}
func (i *IdentityClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { func (i *IdentityClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
return i.identity, nil return i.identity, nil
} }

View File

@@ -139,6 +139,10 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
return id, nil return id, nil
} }
func (s *JWT) IsEnabled() bool {
return s.cfg.JWTAuth.Enabled
}
// remove sensitive query param // remove sensitive query param
// avoid JWT URL login passing auth_token in URL // avoid JWT URL login passing auth_token in URL
func (s *JWT) stripSensitiveParam(httpRequest *http.Request) { func (s *JWT) stripSensitiveParam(httpRequest *http.Request) {

View File

@@ -199,6 +199,15 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
}, nil }, nil
} }
func (c *OAuth) IsEnabled() bool {
provider := c.socialService.GetOAuthInfoProvider(c.providerName)
if provider == nil {
return false
}
return provider.Enabled
}
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

View File

@@ -507,6 +507,49 @@ func TestGenPKCECodeVerifier(t *testing.T) {
assert.Len(t, verifier, 128) assert.Len(t, verifier, 128)
} }
func TestIsEnabled(t *testing.T) {
type testCase struct {
desc string
oauthCfg *social.OAuthInfo
expected bool
}
tests := []testCase{
{
desc: "should return false when client is not enabled",
oauthCfg: &social.OAuthInfo{Enabled: false},
expected: false,
},
{
desc: "should return false when client doesnt exists",
oauthCfg: nil,
expected: false,
},
{
desc: "should return true when client is enabled",
oauthCfg: &social.OAuthInfo{Enabled: true},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
fakeSocialSvc := &socialtest.FakeSocialService{
ExpectedAuthInfoProvider: tt.oauthCfg,
}
cfg := setting.NewCfg()
c := ProvideOAuth(
social.GitHubProviderName,
cfg,
nil,
fakeSocialSvc,
&setting.OSSImpl{Cfg: cfg},
featuremgmt.WithFeatures())
assert.Equal(t, tt.expected, c.IsEnabled())
})
}
}
type mockConnector struct { type mockConnector struct {
AuthCodeURLFunc func(state string, opts ...oauth2.AuthCodeOption) string AuthCodeURLFunc func(state string, opts ...oauth2.AuthCodeOption) string
social.SocialConnector social.SocialConnector

View File

@@ -107,6 +107,10 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return nil, clientErr return nil, clientErr
} }
func (c *Proxy) IsEnabled() bool {
return c.cfg.AuthProxy.Enabled
}
// See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync.
// Error here means that we could not find anything in cache, so we can proceed as usual // Error here means that we could not find anything in cache, so we can proceed as usual
func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) { func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) {

View File

@@ -59,6 +59,10 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
}, nil }, nil
} }
func (c *Render) IsEnabled() bool {
return true
}
func (c *Render) Test(ctx context.Context, r *authn.Request) bool { func (c *Render) Test(ctx context.Context, r *authn.Request) bool {
if r.HTTPRequest == nil { if r.HTTPRequest == nil {
return false return false

View File

@@ -78,6 +78,10 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id
return ident, nil return ident, nil
} }
func (s *Session) IsEnabled() bool {
return true
}
func (s *Session) Test(ctx context.Context, r *authn.Request) bool { func (s *Session) Test(ctx context.Context, r *authn.Request) bool {
if s.cfg.LoginCookieName == "" { if s.cfg.LoginCookieName == "" {
return false return false