mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: use contexthandler in standalone handler chain (#90102)
This commit is contained in:
parent
3b6a8775bb
commit
c210617735
@ -189,7 +189,6 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
|
||||
return contexthandler.ProvideService(
|
||||
cfg,
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: authn.AnonymousNamespaceID, SessionToken: &usertoken.UserToken{}}},
|
||||
)
|
||||
}
|
||||
|
@ -43,6 +43,9 @@ type Requester interface {
|
||||
GetOrgName() string
|
||||
// GetAuthID returns external id for entity.
|
||||
GetAuthID() string
|
||||
// GetAllowedKubernetesNamespace returns either "*" or the single namespace this requester has access to
|
||||
// An empty value means the implementation has not specified a kubernetes namespace.
|
||||
GetAllowedKubernetesNamespace() string
|
||||
// GetAuthenticatedBy returns the authentication method used to authenticate the entity.
|
||||
GetAuthenticatedBy() string
|
||||
// IsAuthenticatedBy returns true if entity was authenticated by any of supplied providers.
|
||||
|
@ -9,20 +9,21 @@ var _ Requester = &StaticRequester{}
|
||||
// This is mostly copied from:
|
||||
// https://github.com/grafana/grafana/blob/v11.0.0/pkg/services/user/identity.go#L16
|
||||
type StaticRequester struct {
|
||||
Namespace Namespace
|
||||
UserID int64
|
||||
UserUID string
|
||||
OrgID int64
|
||||
OrgName string
|
||||
OrgRole RoleType
|
||||
Login string
|
||||
Name string
|
||||
DisplayName string
|
||||
Email string
|
||||
EmailVerified bool
|
||||
AuthID string
|
||||
AuthenticatedBy string
|
||||
IsGrafanaAdmin bool
|
||||
Namespace Namespace
|
||||
UserID int64
|
||||
UserUID string
|
||||
OrgID int64
|
||||
OrgName string
|
||||
OrgRole RoleType
|
||||
Login string
|
||||
Name string
|
||||
DisplayName string
|
||||
Email string
|
||||
EmailVerified bool
|
||||
AuthID string
|
||||
AuthenticatedBy string
|
||||
AllowedKubernetesNamespace string
|
||||
IsGrafanaAdmin bool
|
||||
// Permissions grouped by orgID and actions
|
||||
Permissions map[int64]map[string][]string
|
||||
IDToken string
|
||||
@ -123,6 +124,10 @@ func (u *StaticRequester) GetAuthID() string {
|
||||
return u.AuthID
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetAllowedKubernetesNamespace() string {
|
||||
return u.AllowedKubernetesNamespace
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetAuthenticatedBy() string {
|
||||
return u.AuthenticatedBy
|
||||
}
|
||||
|
@ -33,13 +33,16 @@ func WithRequester(handler http.Handler) http.Handler {
|
||||
slices.Contains(info.GetGroups(), user.SystemPrivilegedGroup) {
|
||||
orgId := int64(1)
|
||||
requester = &identity.StaticRequester{
|
||||
Namespace: identity.NamespaceServiceAccount, // system:apiserver
|
||||
UserID: 1,
|
||||
OrgID: orgId,
|
||||
Name: info.GetName(),
|
||||
Login: info.GetName(),
|
||||
OrgRole: identity.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
Namespace: identity.NamespaceServiceAccount, // system:apiserver
|
||||
UserID: 1,
|
||||
OrgID: orgId,
|
||||
Name: info.GetName(),
|
||||
Login: info.GetName(),
|
||||
OrgRole: identity.RoleAdmin,
|
||||
|
||||
IsGrafanaAdmin: true,
|
||||
AllowedKubernetesNamespace: "default",
|
||||
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgId: {
|
||||
"*": {"*"}, // all resources, all scopes
|
||||
|
@ -74,15 +74,15 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{}
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := o.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Options.TracingOptions.TracingService != nil {
|
||||
tracer.InitTracer(o.Options.TracingOptions.TracingService)
|
||||
}
|
||||
|
||||
config, err := o.Config(tracer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer o.factory.Shutdown()
|
||||
|
||||
if err := o.RunAPIServer(config, stopCh); err != nil {
|
||||
|
@ -76,7 +76,7 @@ func (o *APIServerOptions) loadAPIGroupBuilders(ctx context.Context, tracer trac
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error) {
|
||||
func (o *APIServerOptions) Config(tracer tracing.Tracer) (*genericapiserver.RecommendedConfig, error) {
|
||||
if err := o.Options.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts(
|
||||
"localhost", o.AlternateDNS, []net.IP{netutils.ParseIPSloppy("127.0.0.1")},
|
||||
); err != nil {
|
||||
@ -122,6 +122,7 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error)
|
||||
setting.BuildVersion,
|
||||
setting.BuildCommit,
|
||||
setting.BuildBranch,
|
||||
o.factory.GetOptionalMiddlewares(tracer)...,
|
||||
)
|
||||
return serverConfig, err
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
func setupAuthMiddlewareTest(t *testing.T, identity *authn.Identity, authErr error) *contexthandler.ContextHandler {
|
||||
return contexthandler.ProvideService(setting.NewCfg(), tracing.InitializeTracerForTest(), featuremgmt.WithFeatures(), &authntest.FakeService{
|
||||
return contexthandler.ProvideService(setting.NewCfg(), tracing.InitializeTracerForTest(), &authntest.FakeService{
|
||||
ExpectedErr: authErr,
|
||||
ExpectedIdentity: identity,
|
||||
})
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/navtree"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -273,5 +272,5 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, authnService authn.Servic
|
||||
t.Helper()
|
||||
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
return contexthandler.ProvideService(cfg, tracer, featuremgmt.WithFeatures(), authnService)
|
||||
return contexthandler.ProvideService(cfg, tracer, authnService)
|
||||
}
|
||||
|
@ -360,6 +360,7 @@ var wireBasicSet = wire.NewSet(
|
||||
authnimpl.ProvideService,
|
||||
authnimpl.ProvideIdentitySynchronizer,
|
||||
authnimpl.ProvideAuthnService,
|
||||
authnimpl.ProvideAuthnServiceAuthenticateOnly,
|
||||
authnimpl.ProvideRegistration,
|
||||
supportbundlesimpl.ProvideService,
|
||||
extsvcaccounts.ProvideExtSvcAccountsService,
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/mod/semver"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -53,6 +54,7 @@ func SetupConfig(
|
||||
buildVersion string,
|
||||
buildCommit string,
|
||||
buildBranch string,
|
||||
optionalMiddlewares ...web.Middleware,
|
||||
) error {
|
||||
defsGetter := GetOpenAPIDefinitions(builders)
|
||||
serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
|
||||
@ -98,6 +100,14 @@ func SetupConfig(
|
||||
handler := filters.WithTracingHTTPLoggingAttributes(requestHandler)
|
||||
handler = filters.WithRequester(handler)
|
||||
handler = genericapiserver.DefaultBuildHandlerChain(handler, c)
|
||||
|
||||
// If optional middlewares include auth function, they need to happen before DefaultBuildHandlerChain
|
||||
if len(optionalMiddlewares) > 0 {
|
||||
for _, m := range optionalMiddlewares {
|
||||
handler = m(handler)
|
||||
}
|
||||
}
|
||||
|
||||
handler = filters.WithAcceptHeader(handler)
|
||||
handler = filters.WithPathRewriters(handler, pathRewriters)
|
||||
handler = k8stracing.WithTracing(handler, serverConfig.TracerProvider, "KubernetesAPI")
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@ -33,6 +34,9 @@ type APIServerFactory interface {
|
||||
// Given the flags, what can we produce
|
||||
GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error)
|
||||
|
||||
// Any optional middlewares this factory wants configured via apiserver's BuildHandlerChain facility
|
||||
GetOptionalMiddlewares(tracer tracing.Tracer) []web.Middleware
|
||||
|
||||
// Make an API server for a given group+version
|
||||
MakeAPIServer(ctx context.Context, tracer tracing.Tracer, gv schema.GroupVersion) (builder.APIGroupBuilder, error)
|
||||
|
||||
@ -50,6 +54,10 @@ func (p *DummyAPIFactory) GetOptions() options.OptionsProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DummyAPIFactory) GetOptionalMiddlewares(_ tracing.Tracer) []web.Middleware {
|
||||
return []web.Middleware{}
|
||||
}
|
||||
|
||||
func (p *DummyAPIFactory) GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) {
|
||||
gv := []schema.GroupVersion{}
|
||||
for _, cfg := range runtime {
|
||||
|
@ -73,9 +73,13 @@ type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) er
|
||||
type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
|
||||
type PreLogoutHookFn func(ctx context.Context, requester identity.Requester, sessionToken *usertoken.UserToken) error
|
||||
|
||||
type Service interface {
|
||||
type Authenticator interface {
|
||||
// Authenticate authenticates a request
|
||||
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Authenticator
|
||||
// RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication.
|
||||
// A lower number means higher priority.
|
||||
RegisterPostAuthHook(hook PostAuthHookFn, priority uint)
|
||||
@ -115,10 +119,9 @@ type IdentitySynchronizer interface {
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Authenticator
|
||||
// Name returns the name of a client
|
||||
Name() string
|
||||
// Authenticate performs the authentication for the request
|
||||
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
||||
// IsEnabled returns the enabled status of the client
|
||||
IsEnabled() bool
|
||||
}
|
||||
|
@ -39,6 +39,11 @@ func ProvideAuthnService(s *Service) authn.Service {
|
||||
return s
|
||||
}
|
||||
|
||||
// make sure service also implements authn.ServiceAuthenticateOnly interface
|
||||
func ProvideAuthnServiceAuthenticateOnly(s *Service) authn.Authenticator {
|
||||
return s
|
||||
}
|
||||
|
||||
// make sure service implements authn.IdentitySynchronizer interface
|
||||
func ProvideIdentitySynchronizer(s *Service) authn.IdentitySynchronizer {
|
||||
return s
|
||||
|
@ -20,9 +20,8 @@ import (
|
||||
var _ authn.Client = new(ExtendedJWT)
|
||||
|
||||
const (
|
||||
extJWTAuthenticationHeaderName = "X-Access-Token"
|
||||
extJWTAuthorizationHeaderName = "X-Grafana-Id"
|
||||
extJWTAccessTokenExpectAudience = "grafana"
|
||||
ExtJWTAuthenticationHeaderName = "X-Access-Token"
|
||||
ExtJWTAuthorizationHeaderName = "X-Grafana-Id"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -46,7 +45,7 @@ func ProvideExtendedJWT(cfg *setting.Cfg) *ExtendedJWT {
|
||||
})
|
||||
|
||||
accessTokenVerifier := authlib.NewAccessTokenVerifier(authlib.VerifierConfig{
|
||||
AllowedAudiences: []string{extJWTAccessTokenExpectAudience},
|
||||
AllowedAudiences: cfg.ExtJWTAuth.Audiences,
|
||||
}, keys)
|
||||
|
||||
// For ID tokens, we explicitly do not validate audience, hence an empty AllowedAudiences
|
||||
@ -129,11 +128,19 @@ func (s *ExtendedJWT) authenticateAsUser(
|
||||
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", userID.String())
|
||||
}
|
||||
|
||||
// For use in service layer, allow higher privilege
|
||||
allowedKubernetesNamespace := accessTokenClaims.Rest.Namespace
|
||||
if len(s.cfg.StackID) > 0 {
|
||||
// For single-tenant cloud use, choose the lower of the two (id token will always have the specific namespace)
|
||||
allowedKubernetesNamespace = idTokenClaims.Rest.Namespace
|
||||
}
|
||||
|
||||
return &authn.Identity{
|
||||
ID: userID,
|
||||
OrgID: s.getDefaultOrgID(),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: accessID.String(),
|
||||
ID: userID,
|
||||
OrgID: s.getDefaultOrgID(),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: accessID.String(),
|
||||
AllowedKubernetesNamespace: allowedKubernetesNamespace,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
@ -159,11 +166,12 @@ func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.Acces
|
||||
}
|
||||
|
||||
return &authn.Identity{
|
||||
ID: id,
|
||||
UID: id,
|
||||
OrgID: s.getDefaultOrgID(),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: claims.Subject,
|
||||
ID: id,
|
||||
UID: id,
|
||||
OrgID: s.getDefaultOrgID(),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: claims.Subject,
|
||||
AllowedKubernetesNamespace: claims.Rest.Namespace,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
@ -208,7 +216,7 @@ func (s *ExtendedJWT) Priority() uint {
|
||||
|
||||
// retrieveAuthenticationToken retrieves the JWT token from the request.
|
||||
func (s *ExtendedJWT) retrieveAuthenticationToken(httpRequest *http.Request) string {
|
||||
jwtToken := httpRequest.Header.Get(extJWTAuthenticationHeaderName)
|
||||
jwtToken := httpRequest.Header.Get(ExtJWTAuthenticationHeaderName)
|
||||
|
||||
// Strip the 'Bearer' prefix if it exists.
|
||||
return strings.TrimPrefix(jwtToken, "Bearer ")
|
||||
@ -216,7 +224,7 @@ func (s *ExtendedJWT) retrieveAuthenticationToken(httpRequest *http.Request) str
|
||||
|
||||
// retrieveAuthorizationToken retrieves the JWT token from the request.
|
||||
func (s *ExtendedJWT) retrieveAuthorizationToken(httpRequest *http.Request) string {
|
||||
jwtToken := httpRequest.Header.Get(extJWTAuthorizationHeaderName)
|
||||
jwtToken := httpRequest.Header.Get(ExtJWTAuthorizationHeaderName)
|
||||
|
||||
// Strip the 'Bearer' prefix if it exists.
|
||||
return strings.TrimPrefix(jwtToken, "Bearer ")
|
||||
|
@ -51,6 +51,17 @@ var (
|
||||
Namespace: "default", // org ID of 1 is special and translates to default
|
||||
},
|
||||
}
|
||||
validIDTokenClaimsWithStackSet = idTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Subject: "user:2",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
Rest: authnlib.IDTokenClaims{
|
||||
AuthenticatedBy: "extended_jwt",
|
||||
Namespace: "stack-1234",
|
||||
},
|
||||
}
|
||||
validAcessTokenClaimsWildcard = accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Subject: "access-policy:this-uid",
|
||||
@ -183,6 +194,7 @@ func TestExtendedJWT_Test(t *testing.T) {
|
||||
func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
cfg *setting.Cfg // optional, only used when overriding the cfg provided by default test setup
|
||||
accessToken *accessTokenClaims
|
||||
idToken *idTokenClaims
|
||||
orgID int64
|
||||
@ -195,11 +207,12 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
accessToken: &validAccessTokenClaims,
|
||||
orgID: 1,
|
||||
want: &authn.Identity{
|
||||
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
UID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
OrgID: 1,
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
UID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
OrgID: 1,
|
||||
AllowedKubernetesNamespace: "default",
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{Roles: []string{"fixed:folders:reader"}}},
|
||||
@ -210,11 +223,12 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
accessToken: &validAcessTokenClaimsWildcard,
|
||||
orgID: 1,
|
||||
want: &authn.Identity{
|
||||
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
UID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
OrgID: 1,
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
UID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||
OrgID: 1,
|
||||
AllowedKubernetesNamespace: "*",
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
},
|
||||
@ -226,10 +240,11 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
idToken: &validIDTokenClaims,
|
||||
orgID: 1,
|
||||
want: &authn.Identity{
|
||||
ID: authn.MustParseNamespaceID("user:2"),
|
||||
OrgID: 1,
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ID: authn.MustParseNamespaceID("user:2"),
|
||||
OrgID: 1,
|
||||
AllowedKubernetesNamespace: "default",
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ClientParams: authn.ClientParams{
|
||||
FetchSyncedUser: true,
|
||||
SyncPermissions: true,
|
||||
@ -245,10 +260,36 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
idToken: &validIDTokenClaims,
|
||||
orgID: 1,
|
||||
want: &authn.Identity{
|
||||
ID: authn.MustParseNamespaceID("user:2"),
|
||||
OrgID: 1,
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ID: authn.MustParseNamespaceID("user:2"),
|
||||
OrgID: 1,
|
||||
AllowedKubernetesNamespace: "*",
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ClientParams: authn.ClientParams{
|
||||
FetchSyncedUser: true,
|
||||
SyncPermissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should authenticate as user using wildcard namespace for access token, setting allowed namespace to specific",
|
||||
accessToken: &validAcessTokenClaimsWildcard,
|
||||
idToken: &validIDTokenClaimsWithStackSet,
|
||||
orgID: 1,
|
||||
cfg: &setting.Cfg{
|
||||
// default org set up by the authenticator is 1
|
||||
StackID: "1234",
|
||||
ExtJWTAuth: setting.ExtJWTSettings{
|
||||
Enabled: true,
|
||||
ExpectIssuer: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
want: &authn.Identity{
|
||||
ID: authn.MustParseNamespaceID("user:2"),
|
||||
OrgID: 1,
|
||||
AllowedKubernetesNamespace: "stack-1234",
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ClientParams: authn.ClientParams{
|
||||
FetchSyncedUser: true,
|
||||
SyncPermissions: true,
|
||||
@ -301,7 +342,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
env := setupTestCtx(nil)
|
||||
env := setupTestCtx(tc.cfg)
|
||||
|
||||
validHTTPReq := &http.Request{
|
||||
Header: map[string][]string{
|
||||
@ -313,7 +354,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
if tc.idToken != nil {
|
||||
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.accessToken}
|
||||
env.s.idTokenVerifier = &mockIDVerifier{Claims: *tc.idToken}
|
||||
validHTTPReq.Header.Add(extJWTAuthorizationHeaderName, generateIDToken(*tc.idToken, pk, jose.RS256))
|
||||
validHTTPReq.Header.Add(ExtJWTAuthorizationHeaderName, generateIDToken(*tc.idToken, pk, jose.RS256))
|
||||
}
|
||||
|
||||
id, err := env.s.Authenticate(context.Background(), &authn.Request{
|
||||
|
@ -47,6 +47,8 @@ type Identity struct {
|
||||
// AuthId is the unique identifier for the entity in the external system.
|
||||
// Empty if the identity is provided by Grafana.
|
||||
AuthID string
|
||||
// AllowedKubernetesNamespace
|
||||
AllowedKubernetesNamespace string
|
||||
// IsDisabled is true if the entity is disabled.
|
||||
IsDisabled bool
|
||||
// HelpFlags1 is the help flags for the entity.
|
||||
@ -127,6 +129,10 @@ func (i *Identity) GetLogin() string {
|
||||
return i.Login
|
||||
}
|
||||
|
||||
func (i *Identity) GetAllowedKubernetesNamespace() string {
|
||||
return i.AllowedKubernetesNamespace
|
||||
}
|
||||
|
||||
func (i *Identity) GetOrgID() int64 {
|
||||
return i.OrgID
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
authnClients "github.com/grafana/grafana/pkg/services/authn/clients"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
@ -16,29 +17,26 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureToggles, authnService authn.Service,
|
||||
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, authenticator authn.Authenticator,
|
||||
) *ContextHandler {
|
||||
return &ContextHandler{
|
||||
Cfg: cfg,
|
||||
tracer: tracer,
|
||||
features: features,
|
||||
authnService: authnService,
|
||||
Cfg: cfg,
|
||||
tracer: tracer,
|
||||
authenticator: authenticator,
|
||||
}
|
||||
}
|
||||
|
||||
// ContextHandler is a middleware.
|
||||
type ContextHandler struct {
|
||||
Cfg *setting.Cfg
|
||||
tracer tracing.Tracer
|
||||
features featuremgmt.FeatureToggles
|
||||
authnService authn.Service
|
||||
Cfg *setting.Cfg
|
||||
tracer tracing.Tracer
|
||||
authenticator authn.Authenticator
|
||||
}
|
||||
|
||||
type reqContextKey = ctxkey.Key
|
||||
@ -112,7 +110,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
||||
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
||||
}
|
||||
|
||||
id, err := h.authnService.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req})
|
||||
id, err := h.authenticator.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req})
|
||||
if err != nil {
|
||||
// Hack: set all errors on LookupTokenErr, so we can check it in auth middlewares
|
||||
reqContext.LookupTokenErr = err
|
||||
@ -124,6 +122,8 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
||||
reqContext.IsRenderCall = id.IsAuthenticatedBy(login.RenderModule)
|
||||
}
|
||||
|
||||
h.excludeSensitiveHeadersFromRequest(reqContext.Req)
|
||||
|
||||
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login)
|
||||
span.AddEvent("user", trace.WithAttributes(
|
||||
attribute.String("uname", reqContext.Login),
|
||||
@ -142,6 +142,11 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ContextHandler) excludeSensitiveHeadersFromRequest(req *http.Request) {
|
||||
req.Header.Del(authnClients.ExtJWTAuthenticationHeaderName)
|
||||
req.Header.Del(authnClients.ExtJWTAuthorizationHeaderName)
|
||||
}
|
||||
|
||||
func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) web.BeforeFunc {
|
||||
return func(w web.ResponseWriter) {
|
||||
if w.Written() {
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -27,7 +26,6 @@ func TestContextHandler(t *testing.T) {
|
||||
handler := contexthandler.ProvideService(
|
||||
setting.NewCfg(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedErr: errors.New("some error")},
|
||||
)
|
||||
|
||||
@ -49,7 +47,6 @@ func TestContextHandler(t *testing.T) {
|
||||
handler := contexthandler.ProvideService(
|
||||
setting.NewCfg(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedIdentity: id},
|
||||
)
|
||||
|
||||
@ -75,7 +72,6 @@ func TestContextHandler(t *testing.T) {
|
||||
handler := contexthandler.ProvideService(
|
||||
setting.NewCfg(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedIdentity: identity},
|
||||
)
|
||||
|
||||
@ -97,7 +93,6 @@ func TestContextHandler(t *testing.T) {
|
||||
handler := contexthandler.ProvideService(
|
||||
setting.NewCfg(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedIdentity: identity},
|
||||
)
|
||||
|
||||
@ -128,7 +123,6 @@ func TestContextHandler(t *testing.T) {
|
||||
handler := contexthandler.ProvideService(
|
||||
cfg,
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedIdentity: &authn.Identity{}},
|
||||
)
|
||||
|
||||
@ -154,7 +148,6 @@ func TestContextHandler(t *testing.T) {
|
||||
handler := contexthandler.ProvideService(
|
||||
cfg,
|
||||
tracing.InitializeTracerForTest(),
|
||||
featuremgmt.WithFeatures(),
|
||||
&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID(id)}},
|
||||
)
|
||||
|
||||
|
@ -27,7 +27,9 @@ type SignedInUser struct {
|
||||
// AuthID will be set if user signed in using external method
|
||||
AuthID string
|
||||
// AuthenticatedBy be set if user signed in using external method
|
||||
AuthenticatedBy string
|
||||
AuthenticatedBy string
|
||||
AllowedKubernetesNamespace string
|
||||
|
||||
ApiKeyID int64 `xorm:"api_key_id"`
|
||||
IsServiceAccount bool `xorm:"is_service_account"`
|
||||
IsGrafanaAdmin bool
|
||||
@ -89,6 +91,10 @@ func (u *SignedInUser) HasUniqueId() bool {
|
||||
return u.IsRealUser() || u.IsApiKeyUser() || u.IsServiceAccountUser()
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetAllowedKubernetesNamespace() string {
|
||||
return u.AllowedKubernetesNamespace
|
||||
}
|
||||
|
||||
// GetCacheKey returns a unique key for the entity.
|
||||
// Add an extra prefix to avoid collisions with other caches
|
||||
func (u *SignedInUser) GetCacheKey() string {
|
||||
|
@ -2,6 +2,10 @@ package setting
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
extJWTAccessTokenExpectAudience = "grafana"
|
||||
)
|
||||
|
||||
type AuthJWTSettings struct {
|
||||
// JWT Auth
|
||||
Enabled bool
|
||||
@ -29,6 +33,7 @@ type ExtJWTSettings struct {
|
||||
Enabled bool
|
||||
ExpectIssuer string
|
||||
JWKSUrl string
|
||||
Audiences []string
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readAuthExtJWTSettings() {
|
||||
@ -36,6 +41,9 @@ func (cfg *Cfg) readAuthExtJWTSettings() {
|
||||
jwtSettings := ExtJWTSettings{}
|
||||
jwtSettings.Enabled = authExtendedJWT.Key("enabled").MustBool(false)
|
||||
jwtSettings.JWKSUrl = authExtendedJWT.Key("jwks_url").MustString("")
|
||||
// for Grafana, this is hard coded, but we leave it as a configurable param for other use-cases
|
||||
jwtSettings.Audiences = []string{extJWTAccessTokenExpectAudience}
|
||||
|
||||
cfg.ExtJWTAuth = jwtSettings
|
||||
}
|
||||
|
||||
|
@ -139,6 +139,17 @@ func mwFromHandler(handler Handler) Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
// a convenience function that is provided for users of contexthandler package (standalone apiservers)
|
||||
// who have an implicit dependency on Macron in context but don't want to take a dependency on
|
||||
// router additionally
|
||||
func EmptyMacronMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
m := New()
|
||||
c := m.createContext(writer, request)
|
||||
next.ServeHTTP(writer, c.Req) // since c.Req has the newer context attached
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Context {
|
||||
// NOTE: we have to explicitly copy the middleware chain here to avoid
|
||||
// passing a shared slice to the *Context, which leads to racy behavior in
|
||||
|
Loading…
Reference in New Issue
Block a user