K8s: namespace mapper should use authlib's util (#92332)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user