diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a6d4148b24..7a998bb1099 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -115,7 +115,7 @@ /pkg/services/annotations/ @grafana/grafana-search-and-storage /pkg/services/apikey/ @grafana/identity-squad /pkg/services/cleanup/ @grafana/grafana-backend-group -/pkg/services/contexthandler/ @grafana/grafana-backend-group +/pkg/services/contexthandler/ @grafana/grafana-backend-group @grafana/grafana-app-platform-squad /pkg/services/correlations/ @grafana/explore-squad /pkg/services/dashboardimport/ @grafana/grafana-backend-group /pkg/services/dashboards/ @grafana/grafana-app-platform-squad @@ -320,7 +320,7 @@ /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources /e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend -/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend +/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend # Packages /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index 3a9baa8b22d..e407c9f6061 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -8,7 +8,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" authnlib "github.com/grafana/authlib/authn" - authnlibclaims "github.com/grafana/authlib/claims" + "github.com/grafana/authlib/claims" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sync/singleflight" @@ -85,7 +85,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri s.logger.FromContext(ctx).Debug("Sign new id token", "id", id.GetID()) now := time.Now() - claims := &auth.IDClaims{ + idClaims := &auth.IDClaims{ Claims: &jwt.Claims{ Issuer: s.cfg.AppURL, Audience: getAudience(id.GetOrgID()), @@ -100,15 +100,15 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri }, } - if id.IsIdentityType(authnlibclaims.TypeUser) { - claims.Rest.Email = id.GetEmail() - claims.Rest.EmailVerified = id.IsEmailVerified() - claims.Rest.AuthenticatedBy = id.GetAuthenticatedBy() - claims.Rest.Username = id.GetLogin() - claims.Rest.DisplayName = id.GetDisplayName() + if id.IsIdentityType(claims.TypeUser) { + idClaims.Rest.Email = id.GetEmail() + idClaims.Rest.EmailVerified = id.IsEmailVerified() + idClaims.Rest.AuthenticatedBy = id.GetAuthenticatedBy() + idClaims.Rest.Username = id.GetLogin() + idClaims.Rest.DisplayName = id.GetDisplayName() } - token, err := s.signer.SignIDToken(ctx, claims) + token, err := s.signer.SignIDToken(ctx, idClaims) if err != nil { s.metrics.failedTokenSigningCounter.Inc() return resultType{}, nil @@ -124,7 +124,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri s.logger.FromContext(ctx).Error("Failed to add id token to cache", "error", err) } - return resultType{token: token, idClaims: claims}, nil + return resultType{token: token, idClaims: idClaims}, nil }) if err != nil { @@ -140,7 +140,7 @@ func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) erro func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { // FIXME(kalleep): we should probably lazy load this - token, claims, err := s.SignIdentity(ctx, identity) + token, idClaims, err := s.SignIdentity(ctx, identity) if err != nil { if shouldLogErr(err) { s.logger.FromContext(ctx).Error("Failed to sign id token", "err", err, "id", identity.GetID()) @@ -150,7 +150,7 @@ func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.R } identity.IDToken = token - identity.IDTokenClaims = claims + identity.IDTokenClaims = idClaims return nil } diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 29dba930e94..db7285c4729 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -74,13 +74,11 @@ type ExtendedJWT struct { func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest) - accessToken, err := s.accessTokenVerifier.Verify(ctx, jwtToken) + accessTokenClaims, 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) @@ -88,10 +86,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(authlib.NewIdentityClaims(*idTokenClaims), accessTokenClaims) + return s.authenticateAsUser(*idTokenClaims, *accessTokenClaims) } - return s.authenticateAsService(accessTokenClaims) + return s.authenticateAsService(*accessTokenClaims) } func (s *ExtendedJWT) IsEnabled() bool { @@ -99,73 +97,75 @@ func (s *ExtendedJWT) IsEnabled() bool { } func (s *ExtendedJWT) authenticateAsUser( - idTokenClaims claims.IdentityClaims, - accessTokenClaims claims.AccessClaims, + idTokenClaims authlib.Claims[authlib.IDTokenClaims], + accessTokenClaims authlib.Claims[authlib.AccessTokenClaims], ) (*authn.Identity, error) { // Only allow id tokens signed for namespace configured for this instance. - if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(idTokenClaims, allowedNamespace) { - return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Namespace()) + if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(authlib.NewIdentityClaims(idTokenClaims), allowedNamespace) { + return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Rest.Namespace) } // Allow access tokens with either the same namespace as the validated id token namespace or wildcard (`*`). - if !claims.NamespaceMatches(accessTokenClaims, idTokenClaims.Namespace()) { - return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Namespace()) + if !claims.NamespaceMatches(authlib.NewAccessClaims(accessTokenClaims), idTokenClaims.Rest.Namespace) { + return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.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 !claims.IsIdentityType(accessType, claims.TypeAccessPolicy) { - return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessTokenClaims.Subject()) + 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 !claims.IsIdentityType(t, claims.TypeUser) { - return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject()) + return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject) } // For use in service layer, allow higher privilege - allowedKubernetesNamespace := accessTokenClaims.Namespace() + 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.Namespace() + allowedKubernetesNamespace = idTokenClaims.Rest.Namespace } return &authn.Identity{ ID: id, Type: t, OrgID: s.getDefaultOrgID(), + AccessTokenClaims: &accessTokenClaims, + IDTokenClaims: &idTokenClaims, AuthenticatedBy: login.ExtendedJWTModule, - AuthID: accessTokenClaims.Subject(), + AuthID: accessTokenClaims.Subject, AllowedKubernetesNamespace: allowedKubernetesNamespace, ClientParams: authn.ClientParams{ SyncPermissions: true, FetchPermissionsParams: authn.FetchPermissionsParams{ - ActionsLookup: accessTokenClaims.DelegatedPermissions(), + ActionsLookup: accessTokenClaims.Rest.DelegatedPermissions, }, FetchSyncedUser: true, }}, nil } -func (s *ExtendedJWT) authenticateAsService(accessTokenClaims claims.AccessClaims) (*authn.Identity, error) { +func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*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.NamespaceMatches(accessTokenClaims, allowedNamespace) { - return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", accessTokenClaims.Namespace()) + if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(authlib.NewAccessClaims(accessTokenClaims), allowedNamespace) { + return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace) } - t, id, err := identity.ParseTypeAndID(accessTokenClaims.Subject()) + t, id, err := identity.ParseTypeAndID(accessTokenClaims.Subject) if err != nil { return nil, fmt.Errorf("failed to parse access token subject: %w", err) } if !claims.IsIdentityType(t, claims.TypeAccessPolicy) { - return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject()) + return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject) } return &authn.Identity{ @@ -173,13 +173,15 @@ func (s *ExtendedJWT) authenticateAsService(accessTokenClaims claims.AccessClaim UID: id, Type: t, OrgID: s.getDefaultOrgID(), + AccessTokenClaims: &accessTokenClaims, + IDTokenClaims: nil, AuthenticatedBy: login.ExtendedJWTModule, - AuthID: accessTokenClaims.Subject(), - AllowedKubernetesNamespace: accessTokenClaims.Namespace(), + AuthID: accessTokenClaims.Subject, + AllowedKubernetesNamespace: accessTokenClaims.Rest.Namespace, ClientParams: authn.ClientParams{ SyncPermissions: true, FetchPermissionsParams: authn.FetchPermissionsParams{ - Roles: accessTokenClaims.Permissions(), + Roles: accessTokenClaims.Rest.Permissions, }, FetchSyncedUser: false, }, diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index f148a09cc3f..7e1fc352117 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -230,6 +230,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { UID: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaims, AllowedKubernetesNamespace: "default", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -247,6 +248,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { UID: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWildcard, AllowedKubernetesNamespace: "*", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -264,6 +266,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ID: "2", Type: claims.TypeUser, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaims, + IDTokenClaims: &validIDTokenClaims, AllowedKubernetesNamespace: "default", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -285,6 +289,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ID: "2", Type: claims.TypeUser, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWildcard, + IDTokenClaims: &validIDTokenClaims, AllowedKubernetesNamespace: "*", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -311,6 +317,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ID: "2", Type: claims.TypeUser, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWildcard, + IDTokenClaims: &validIDTokenClaimsWithStackSet, AllowedKubernetesNamespace: "stacks-1234", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -337,6 +345,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { UID: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWithStackSet, AllowedKubernetesNamespace: "stacks-1234", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -362,6 +371,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { UID: "this-uid", Type: claims.TypeAccessPolicy, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet, AllowedKubernetesNamespace: "stack-1234", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -387,6 +397,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ID: "2", Type: claims.TypeUser, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet, + IDTokenClaims: &validIDTokenClaimsWithDeprecatedStackClaimSet, AllowedKubernetesNamespace: "stack-1234", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", @@ -413,6 +425,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ID: "2", Type: claims.TypeUser, OrgID: 1, + AccessTokenClaims: &validAccessTokenClaimsWildcard, + IDTokenClaims: &validIDTokenClaimsWithStackSet, AllowedKubernetesNamespace: "stacks-1234", AuthenticatedBy: "extendedjwt", AuthID: "access-policy:this-uid", diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index 4a0da892d8d..b99b5703712 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -74,10 +74,15 @@ type Identity struct { // IDToken is a signed token representing the identity that can be forwarded to plugins and external services. IDToken string IDTokenClaims *authn.Claims[authn.IDTokenClaims] + + AccessTokenClaims *authn.Claims[authn.AccessTokenClaims] } // Access implements claims.AuthInfo. func (i *Identity) GetAccess() claims.AccessClaims { + if i.AccessTokenClaims != nil { + return authn.NewAccessClaims(*i.AccessTokenClaims) + } return &identity.IDClaimsWrapper{Source: i} } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index a30f560b414..b82bd0c3ed8 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -122,6 +122,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { reqContext.IsSignedIn = !reqContext.SignedInUser.IsAnonymous reqContext.AllowAnonymous = reqContext.SignedInUser.IsAnonymous reqContext.IsRenderCall = id.IsAuthenticatedBy(login.RenderModule) + ctx = identity.WithRequester(ctx, id) } h.excludeSensitiveHeadersFromRequest(reqContext.Req) @@ -139,8 +140,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { // End the span to make next handlers not wrapped within middleware span span.End() - - next.ServeHTTP(w, r.WithContext(identity.WithRequester(ctx, id))) + next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index b680d848e61..06396ffc779 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -9841,6 +9841,12 @@ "$ref": "#/definitions/SnapshotListResponseDTO" } }, + "statusMovedPermanently": { + "description": "StatusMovedPermanently", + "schema": { + "$ref": "#/definitions/ErrorResponseBody" + } + }, "unauthorisedError": { "description": "UnauthorizedError is returned when the request is not authenticated.", "schema": {