From af2e79aa83fba036059649d024551ab28d431379 Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:01:42 -0700 Subject: [PATCH] K8s: namespace mapper should use authlib's util (#92332) --- go.mod | 6 +- go.sum | 12 +- go.work.sum | 2 + pkg/apimachinery/go.mod | 4 +- pkg/apimachinery/go.sum | 8 +- pkg/apimachinery/identity/wrapper.go | 4 + pkg/apiserver/go.mod | 2 +- pkg/apiserver/go.sum | 4 +- .../apiserver/endpoints/request/namespace.go | 9 +- .../endpoints/request/namespace_test.go | 7 +- pkg/services/authn/clients/ext_jwt.go | 62 ++++---- pkg/services/authn/clients/ext_jwt_test.go | 149 ++++++++++++++++-- pkg/services/authz/client.go | 5 +- pkg/storage/unified/apistore/go.mod | 4 +- pkg/storage/unified/apistore/go.sum | 8 +- pkg/storage/unified/resource/go.mod | 4 +- pkg/storage/unified/resource/go.sum | 8 +- .../unified/resource/grpc/authenticator.go | 4 +- 18 files changed, 217 insertions(+), 85 deletions(-) diff --git a/go.mod b/go.mod index 54cf82071fe..312ff334f7f 100644 --- a/go.mod +++ b/go.mod @@ -74,9 +74,9 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240827075410-70248a7a3a67 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team - github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team + github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41 // @grafana/alerting-backend + github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // @grafana/identity-access-team + github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 952ffb51aa1..3b26c22c5ab 100644 --- a/go.sum +++ b/go.sum @@ -2246,12 +2246,12 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240827075410-70248a7a3a67 h1:3spByRvTR3Qo7uDCEVVLB7+5VYH1q4hxwqVLdNpcS6k= -github.com/grafana/alerting v0.0.0-20240827075410-70248a7a3a67/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41 h1:p+UsX43BoDH5YlG6xUd9xDS3M4sWouy8VJ+ODv5S6uE= +github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= diff --git a/go.work.sum b/go.work.sum index 448f00a3af1..8e064f51b13 100644 --- a/go.work.sum +++ b/go.work.sum @@ -714,6 +714,8 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grafana/authlib v0.0.0-20240730122259-a0d13672efb1/go.mod h1:YA9We4kTafu7mlMnUh3In6Q2wpg8fYN3ycgCKOK1TB8= github.com/grafana/authlib/claims v0.0.0-20240809101159-74eaccc31a06/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib/claims v0.0.0-20240827173836-433b4fdb2f25/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib/claims v0.0.0-20240827201239-81213c6670d3/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= github.com/grafana/grafana-plugin-sdk-go v0.235.0/go.mod h1:6n9LbrjGL3xAATntYVNcIi90G9BVHRJjzHKz5FXVfWw= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 418e3beab91..7e46f1a30e6 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.23.0 require ( - github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team - github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // @grafana/identity-access-team + github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/stretchr/testify v1.9.0 k8s.io/apimachinery v0.31.0 k8s.io/apiserver v0.31.0 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 66b4df6cb6c..aba77b5e0e7 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -28,10 +28,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/pkg/apimachinery/identity/wrapper.go b/pkg/apimachinery/identity/wrapper.go index 299a977a3ca..22dcb32dd0f 100644 --- a/pkg/apimachinery/identity/wrapper.go +++ b/pkg/apimachinery/identity/wrapper.go @@ -13,6 +13,10 @@ type IDClaimsWrapper struct { Source Requester } +func (i *IDClaimsWrapper) IsNil() bool { + return i.Source.IsNil() +} + // GetAuthenticatedBy implements claims.IdentityClaims. func (i *IDClaimsWrapper) AuthenticatedBy() string { return i.Source.GetAuthenticatedBy() diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 5e7937c7e4c..cd4c090a84b 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/google/go-cmp v0.6.0 - github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db + github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 github.com/prometheus/client_golang v1.20.0 github.com/stretchr/testify v1.9.0 diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 66724337e95..ffa91e73824 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -77,8 +77,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 h1:ItDcDxUjVLPKja+hogpqgW/kj8LxUL2qscelXIsN1Bs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1/go.mod h1:DkxMin+qOh1Fgkxfbt+CUfBqqsCQJMG9op8Os/irBPA= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= diff --git a/pkg/services/apiserver/endpoints/request/namespace.go b/pkg/services/apiserver/endpoints/request/namespace.go index 285b5e40273..0a69607f9e0 100644 --- a/pkg/services/apiserver/endpoints/request/namespace.go +++ b/pkg/services/apiserver/endpoints/request/namespace.go @@ -3,6 +3,7 @@ package request import ( "context" "fmt" + "strconv" "k8s.io/apiserver/pkg/endpoints/request" @@ -17,8 +18,12 @@ type NamespaceMapper = claims.NamespaceFormatter // GetNamespaceMapper returns a function that will convert orgIds into a consistent namespace func GetNamespaceMapper(cfg *setting.Cfg) NamespaceMapper { if cfg != nil && cfg.StackID != "" { - //val := claims.CloudNamespaceFormatter(cfg.Sta) - return func(orgId int64) string { return "stack-" + cfg.StackID } + stackIdInt, err := strconv.ParseInt(cfg.StackID, 10, 64) + if err != nil { + stackIdInt = 0 + } + cloudNamespace := claims.CloudNamespaceFormatter(stackIdInt) + return func(_ int64) string { return cloudNamespace } } return claims.OrgNamespaceFormatter } diff --git a/pkg/services/apiserver/endpoints/request/namespace_test.go b/pkg/services/apiserver/endpoints/request/namespace_test.go index a42be2605d4..ef99eed29a4 100644 --- a/pkg/services/apiserver/endpoints/request/namespace_test.go +++ b/pkg/services/apiserver/endpoints/request/namespace_test.go @@ -26,11 +26,14 @@ func TestNamespaceMapper(t *testing.T) { orgId: 123, expected: "org-123", }, + // an invalid use-case, but just documenting that it's handled as stack-0 + // this currently prevents the need to have the Mapper return (mapped, err) instead of just mapped. + // err checking is avoided for now to keep the usage fluent { name: "with stackId", cfg: "abc", - orgId: 123, // ignored - expected: "stack-abc", + orgId: 123, // ignored + expected: "stack-0", // we parse to int and default to 0 }, } diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index f1fd557fc91..29dba930e94 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -8,7 +8,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" authlib "github.com/grafana/authlib/authn" - authlibclaims "github.com/grafana/authlib/claims" + "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -74,11 +74,13 @@ type ExtendedJWT struct { func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest) - claims, err := s.accessTokenVerifier.Verify(ctx, jwtToken) + accessToken, err := s.accessTokenVerifier.Verify(ctx, jwtToken) if err != nil { return nil, errExtJWTInvalid.Errorf("failed to verify access token: %w", err) } + accessTokenClaims := authlib.NewAccessClaims(*accessToken) + idToken := s.retrieveAuthorizationToken(r.HTTPRequest) if idToken != "" { idTokenClaims, err := s.idTokenVerifier.Verify(ctx, idToken) @@ -86,10 +88,10 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth return nil, errExtJWTInvalid.Errorf("failed to verify id token: %w", err) } - return s.authenticateAsUser(idTokenClaims, claims) + return s.authenticateAsUser(authlib.NewIdentityClaims(*idTokenClaims), accessTokenClaims) } - return s.authenticateAsService(claims) + return s.authenticateAsService(accessTokenClaims) } func (s *ExtendedJWT) IsEnabled() bool { @@ -97,42 +99,42 @@ func (s *ExtendedJWT) IsEnabled() bool { } func (s *ExtendedJWT) authenticateAsUser( - idTokenClaims *authlib.Claims[authlib.IDTokenClaims], - accessTokenClaims *authlib.Claims[authlib.AccessTokenClaims], + idTokenClaims claims.IdentityClaims, + accessTokenClaims claims.AccessClaims, ) (*authn.Identity, error) { // Only allow id tokens signed for namespace configured for this instance. - if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); idTokenClaims.Rest.Namespace != allowedNamespace { - return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Rest.Namespace) + if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(idTokenClaims, allowedNamespace) { + return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Namespace()) } // Allow access tokens with either the same namespace as the validated id token namespace or wildcard (`*`). - if !accessTokenClaims.Rest.NamespaceMatches(idTokenClaims.Rest.Namespace) { - return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace) + if !claims.NamespaceMatches(accessTokenClaims, idTokenClaims.Namespace()) { + return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Namespace()) } - accessType, _, err := identity.ParseTypeAndID(accessTokenClaims.Subject) + accessType, _, err := identity.ParseTypeAndID(accessTokenClaims.Subject()) if err != nil { - return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject) + return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject()) } - if !authlibclaims.IsIdentityType(accessType, authlibclaims.TypeAccessPolicy) { - return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessTokenClaims.Subject) + if !claims.IsIdentityType(accessType, claims.TypeAccessPolicy) { + return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessTokenClaims.Subject()) } - t, id, err := identity.ParseTypeAndID(idTokenClaims.Subject) + t, id, err := identity.ParseTypeAndID(idTokenClaims.Subject()) if err != nil { return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err) } - if !authlibclaims.IsIdentityType(t, authlibclaims.TypeUser) { - return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject) + if !claims.IsIdentityType(t, claims.TypeUser) { + return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject()) } // For use in service layer, allow higher privilege - allowedKubernetesNamespace := accessTokenClaims.Rest.Namespace + allowedKubernetesNamespace := accessTokenClaims.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 + allowedKubernetesNamespace = idTokenClaims.Namespace() } return &authn.Identity{ @@ -140,30 +142,30 @@ func (s *ExtendedJWT) authenticateAsUser( Type: t, OrgID: s.getDefaultOrgID(), AuthenticatedBy: login.ExtendedJWTModule, - AuthID: accessTokenClaims.Subject, + AuthID: accessTokenClaims.Subject(), AllowedKubernetesNamespace: allowedKubernetesNamespace, ClientParams: authn.ClientParams{ SyncPermissions: true, FetchPermissionsParams: authn.FetchPermissionsParams{ - ActionsLookup: accessTokenClaims.Rest.DelegatedPermissions, + ActionsLookup: accessTokenClaims.DelegatedPermissions(), }, FetchSyncedUser: true, }}, nil } -func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) { +func (s *ExtendedJWT) authenticateAsService(accessTokenClaims claims.AccessClaims) (*authn.Identity, error) { // Allow access tokens with that has a wildcard namespace or a namespace matching this instance. - if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.Rest.NamespaceMatches(allowedNamespace) { - return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", claims.Rest.Namespace) + if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(accessTokenClaims, allowedNamespace) { + return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", accessTokenClaims.Namespace()) } - t, id, err := identity.ParseTypeAndID(claims.Subject) + t, id, err := identity.ParseTypeAndID(accessTokenClaims.Subject()) if err != nil { return nil, fmt.Errorf("failed to parse access token subject: %w", err) } - if !authlibclaims.IsIdentityType(t, authlibclaims.TypeAccessPolicy) { - return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", claims.Subject) + if !claims.IsIdentityType(t, claims.TypeAccessPolicy) { + return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject()) } return &authn.Identity{ @@ -172,12 +174,12 @@ func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.Acces Type: t, OrgID: s.getDefaultOrgID(), AuthenticatedBy: login.ExtendedJWTModule, - AuthID: claims.Subject, - AllowedKubernetesNamespace: claims.Rest.Namespace, + AuthID: accessTokenClaims.Subject(), + AllowedKubernetesNamespace: accessTokenClaims.Namespace(), ClientParams: authn.ClientParams{ SyncPermissions: true, FetchPermissionsParams: authn.FetchPermissionsParams{ - Roles: claims.Rest.Permissions, + Roles: accessTokenClaims.Permissions(), }, FetchSyncedUser: false, }, diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index bc249ac6e46..f148a09cc3f 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -51,6 +51,17 @@ var ( }, } 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: "stacks-1234", + }, + } + validIDTokenClaimsWithDeprecatedStackClaimSet = idTokenClaims{ Claims: &jwt.Claims{ Subject: "user:2", Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)), @@ -61,7 +72,7 @@ var ( Namespace: "stack-1234", }, } - validAcessTokenClaimsWildcard = accessTokenClaims{ + validAccessTokenClaimsWildcard = accessTokenClaims{ Claims: &jwt.Claims{ Subject: "access-policy:this-uid", Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)), @@ -71,15 +82,24 @@ var ( Namespace: "*", }, } - invalidWildcardNamespaceIDTokenClaims = idTokenClaims{ + validAccessTokenClaimsWithStackSet = accessTokenClaims{ Claims: &jwt.Claims{ - Subject: "user:2", + Subject: "access-policy:this-uid", 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: "*", + Rest: authnlib.AccessTokenClaims{ + Namespace: "stacks-1234", + }, + } + validAccessTokenClaimsWithDeprecatedStackClaimSet = accessTokenClaims{ + Claims: &jwt.Claims{ + Subject: "access-policy:this-uid", + 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.AccessTokenClaims{ + Namespace: "stack-1234", }, } invalidNamespaceIDTokenClaims = idTokenClaims{ @@ -220,7 +240,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { }, { name: "should authenticate as service using wildcard namespace", - accessToken: &validAcessTokenClaimsWildcard, + accessToken: &validAccessTokenClaimsWildcard, orgID: 1, want: &authn.Identity{ ID: "this-uid", @@ -258,7 +278,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { }, { name: "should authenticate as user using wildcard namespace for access token", - accessToken: &validAcessTokenClaimsWildcard, + accessToken: &validAccessTokenClaimsWildcard, idToken: &validIDTokenClaims, orgID: 1, want: &authn.Identity{ @@ -276,7 +296,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { }, { name: "should authenticate as user using wildcard namespace for access token, setting allowed namespace to specific", - accessToken: &validAcessTokenClaimsWildcard, + accessToken: &validAccessTokenClaimsWildcard, idToken: &validIDTokenClaimsWithStackSet, orgID: 1, cfg: &setting.Cfg{ @@ -287,6 +307,82 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ExpectIssuer: "http://localhost:3000", }, }, + want: &authn.Identity{ + ID: "2", + Type: claims.TypeUser, + OrgID: 1, + AllowedKubernetesNamespace: "stacks-1234", + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ + FetchSyncedUser: true, + SyncPermissions: true, + }, + }, + }, + { + name: "should authenticate as service using specific namespace claim in access token", + accessToken: &validAccessTokenClaimsWithStackSet, + 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: "this-uid", + UID: "this-uid", + Type: claims.TypeAccessPolicy, + OrgID: 1, + AllowedKubernetesNamespace: "stacks-1234", + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, + }, + }, + { + name: "should authenticate as service using specific deprecated namespace claim in access token", + accessToken: &validAccessTokenClaimsWithDeprecatedStackClaimSet, + 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: "this-uid", + UID: "this-uid", + Type: claims.TypeAccessPolicy, + OrgID: 1, + AllowedKubernetesNamespace: "stack-1234", + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, + }, + }, + { + name: "should authenticate as user using specific deprecated namespace claim in access and id tokens", + accessToken: &validAccessTokenClaimsWithDeprecatedStackClaimSet, + idToken: &validIDTokenClaimsWithDeprecatedStackClaimSet, + 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: "2", Type: claims.TypeUser, @@ -294,6 +390,32 @@ func TestExtendedJWT_Authenticate(t *testing.T) { AllowedKubernetesNamespace: "stack-1234", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ + SyncPermissions: true, + FetchSyncedUser: true, + }, + }, + }, + { + name: "should authenticate as user using wildcard namespace for access token, setting allowed namespace to specific", + accessToken: &validAccessTokenClaimsWildcard, + 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: "2", + Type: claims.TypeUser, + OrgID: 1, + AllowedKubernetesNamespace: "stacks-1234", + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", ClientParams: authn.ClientParams{ FetchSyncedUser: true, SyncPermissions: true, @@ -301,14 +423,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { }, }, { - name: "should return error when id token namespace is a wildcard", - accessToken: &validAccessTokenClaims, - idToken: &invalidWildcardNamespaceIDTokenClaims, - orgID: 1, - wantErr: errExtJWTDisallowedNamespaceClaim, - }, - { - name: "should return error when id token has wildcard namespace", + name: "should return error when id token has an invalid namespace", accessToken: &validAccessTokenClaims, idToken: &invalidNamespaceIDTokenClaims, orgID: 1, diff --git a/pkg/services/authz/client.go b/pkg/services/authz/client.go index 69f68f5cf94..eb22e20de87 100644 --- a/pkg/services/authz/client.go +++ b/pkg/services/authz/client.go @@ -8,6 +8,7 @@ import ( authnlib "github.com/grafana/authlib/authn" authzlib "github.com/grafana/authlib/authz" authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/authlib/claims" grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -105,7 +106,7 @@ func newInProcLegacyClient(server *legacyServer) (authzlib.MultiTenantClient, er &authzlib.MultiTenantClientConfig{}, authzlib.WithGrpcConnectionLCOption(channel), // nolint:staticcheck - authzlib.WithNamespaceFormatterLCOption(authnlib.OnPremNamespaceFormatter), + authzlib.WithNamespaceFormatterLCOption(claims.OrgNamespaceFormatter), authzlib.WithDisableAccessTokenLCOption(), ) } @@ -129,7 +130,7 @@ func newGrpcLegacyClient(address string) (authzlib.MultiTenantClient, error) { grpc.WithStreamInterceptor(clientInterceptor.StreamClientInterceptor), ), // nolint:staticcheck - authzlib.WithNamespaceFormatterLCOption(authnlib.OnPremNamespaceFormatter), + authzlib.WithNamespaceFormatterLCOption(claims.OrgNamespaceFormatter), // TODO(drclau): remove this once we have access token support on-prem authzlib.WithDisableAccessTokenLCOption(), ) diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index cc553c54044..2d22aaa9544 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/storage/unified/apistore go 1.23.0 require ( - github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db + github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240821161612-71f0dae39e9d @@ -43,7 +43,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db // indirect + github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index f779d2b7281..c5276e79cb7 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -161,10 +161,10 @@ github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDP github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da h1:2E3c/I3ayAy4Z1GwIPqXNZcpUccRapE1aBXA1ho4g7o= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da/go.mod h1:p09fvU5ujNL/Ig8HB7g4f+S0zyYbQq3x/f0jA4ujVOM= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da h1:xQMb8cRZYu7D0IO9q/lB7qFQpLGAoPUnCase1CGHrXY= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 360723aab76..db6eaa1bbde 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -4,8 +4,8 @@ go 1.23.0 require ( github.com/fullstorydev/grpchan v1.1.1 - github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db - github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db + github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 + github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808164224-787abccfbc9e github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 github.com/prometheus/client_golang v1.20.0 diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 02d4c05722a..631072b38e6 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -128,10 +128,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg= -github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso= -github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ= +github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= +github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808164224-787abccfbc9e h1:3vNpomyzv714Hgls5vn+fC0vgv8wUOSHepUl7PB5nUs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808164224-787abccfbc9e/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= diff --git a/pkg/storage/unified/resource/grpc/authenticator.go b/pkg/storage/unified/resource/grpc/authenticator.go index 10f3c656508..f1f9aea5bec 100644 --- a/pkg/storage/unified/resource/grpc/authenticator.go +++ b/pkg/storage/unified/resource/grpc/authenticator.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/grafana/authlib/authn" - "github.com/grafana/authlib/claims" + authClaims "github.com/grafana/authlib/claims" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -77,7 +77,7 @@ func (f *Authenticator) decodeMetadata(ctx context.Context, meta metadata.MD) (i // TODO, remove after this has been deployed to unified storage if getter(mdUserID) == "" { var err error - user.Type = claims.TypeUser + user.Type = authClaims.TypeUser user.UserID, err = strconv.ParseInt(getter("grafana-userid"), 10, 64) if err != nil { return nil, fmt.Errorf("invalid user id: %w", err)