Authn: resolve issues with setting up a nil identity (#92620)

This commit is contained in:
Charandas 2024-08-28 14:49:41 -07:00 committed by GitHub
parent 692280cd32
commit 4f024d94d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 71 additions and 44 deletions

4
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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
}

View File

@ -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,
},

View File

@ -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",

View File

@ -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}
}

View File

@ -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))
})
}

View File

@ -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": {