diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 34a57c453f0..c78f704d7b4 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -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{}}}, ) } diff --git a/pkg/apimachinery/identity/requester.go b/pkg/apimachinery/identity/requester.go index 9791077fe5f..f65ec49e91e 100644 --- a/pkg/apimachinery/identity/requester.go +++ b/pkg/apimachinery/identity/requester.go @@ -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. diff --git a/pkg/apimachinery/identity/static.go b/pkg/apimachinery/identity/static.go index 84df3966b80..a76420aa119 100644 --- a/pkg/apimachinery/identity/static.go +++ b/pkg/apimachinery/identity/static.go @@ -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 } diff --git a/pkg/apiserver/endpoints/filters/requester.go b/pkg/apiserver/endpoints/filters/requester.go index d091246d024..fe09802b3c0 100644 --- a/pkg/apiserver/endpoints/filters/requester.go +++ b/pkg/apiserver/endpoints/filters/requester.go @@ -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 diff --git a/pkg/cmd/grafana/apiserver/cmd.go b/pkg/cmd/grafana/apiserver/cmd.go index c5800e55f55..585e00ebb5a 100644 --- a/pkg/cmd/grafana/apiserver/cmd.go +++ b/pkg/cmd/grafana/apiserver/cmd.go @@ -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 { diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go index 8578651f7c9..790c2fe2be8 100644 --- a/pkg/cmd/grafana/apiserver/server.go +++ b/pkg/cmd/grafana/apiserver/server.go @@ -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 } diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index c5fc39ae508..d52c95b9905 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -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, }) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 722c9f09335..06adf52a7a3 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -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) } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 430180993c3..8ca6910b160 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -360,6 +360,7 @@ var wireBasicSet = wire.NewSet( authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, + authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 8c0e84db2e2..52c47813d0e 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -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") diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index edd421e0333..1458859bcd5 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -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 { diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 88707690aab..87e3491dd0b 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -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 } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 847d61fcf9a..18bfab199a2 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -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 diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index f58fc48b892..bbe639ff432 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -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 ") diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 6b8cdcce2a4..2413fb38e23 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -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{ diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index 6133b58d1d1..d1094025c5c 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -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 } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 4612bb0784f..a58425e54ff 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -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() { diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 0dcdf896d61..c237660e237 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -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)}}, ) diff --git a/pkg/services/user/identity.go b/pkg/services/user/identity.go index bc3d8e273ef..acb12b59a6c 100644 --- a/pkg/services/user/identity.go +++ b/pkg/services/user/identity.go @@ -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 { diff --git a/pkg/setting/setting_jwt.go b/pkg/setting/setting_jwt.go index bbc7e3612e6..42b8009de4b 100644 --- a/pkg/setting/setting_jwt.go +++ b/pkg/setting/setting_jwt.go @@ -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 } diff --git a/pkg/web/macaron.go b/pkg/web/macaron.go index ceecf6dc08b..17afc06657a 100644 --- a/pkg/web/macaron.go +++ b/pkg/web/macaron.go @@ -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