mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Authn: resolve issues with setting up a nil identity (#92620)
This commit is contained in:
parent
692280cd32
commit
4f024d94d8
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user