mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[authn] use authlib client+interceptors for in-proc mode (#93124)
* Add authlib gRPC authenticators for in-proc mode * implement `StaticRequester` signing in the unified resource client - [x] when the `claims.AuthInfo` value type is `identity.StaticRequester`, and there's no ID token set, create an internal token and sign it with symmetrical key. This is a workaround for `go-jose` not offering the possibility to create an unsigned token. - [x] update `IDClaimsWrapper` to support the scenario above - [x] Switch to using `claims.From()` in `dashboardSqlAccess.SaveDashboard()` --------- Co-authored-by: gamab <gabriel.mabille@grafana.com>
This commit is contained in:
committed by
GitHub
parent
db97da3465
commit
a8b07b0c81
@@ -76,6 +76,9 @@ func (u *StaticRequester) IsIdentityType(expected ...claims.IdentityType) bool {
|
|||||||
|
|
||||||
// GetExtra implements Requester.
|
// GetExtra implements Requester.
|
||||||
func (u *StaticRequester) GetExtra() map[string][]string {
|
func (u *StaticRequester) GetExtra() map[string][]string {
|
||||||
|
if u.IDToken != "" {
|
||||||
|
return map[string][]string{"id-token": {u.IDToken}}
|
||||||
|
}
|
||||||
return map[string][]string{}
|
return map[string][]string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package identity
|
package identity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/authlib/claims"
|
"github.com/grafana/authlib/claims"
|
||||||
@@ -54,7 +55,7 @@ func (i *IDClaimsWrapper) Username() string {
|
|||||||
|
|
||||||
// GetAudience implements claims.AccessClaims.
|
// GetAudience implements claims.AccessClaims.
|
||||||
func (i *IDClaimsWrapper) Audience() []string {
|
func (i *IDClaimsWrapper) Audience() []string {
|
||||||
return []string{}
|
return []string{fmt.Sprintf("org:%d", i.Source.GetOrgID())}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDelegatedPermissions implements claims.AccessClaims.
|
// GetDelegatedPermissions implements claims.AccessClaims.
|
||||||
@@ -104,5 +105,5 @@ func (i *IDClaimsWrapper) Scopes() []string {
|
|||||||
|
|
||||||
// GetSubject implements claims.AccessClaims.
|
// GetSubject implements claims.AccessClaims.
|
||||||
func (i *IDClaimsWrapper) Subject() string {
|
func (i *IDClaimsWrapper) Subject() string {
|
||||||
return ""
|
return i.Source.GetID()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/authlib/claims"
|
"github.com/grafana/authlib/claims"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
@@ -23,8 +26,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/utils/ptr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -347,10 +348,11 @@ func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, u
|
|||||||
// SaveDashboard implements DashboardAccess.
|
// SaveDashboard implements DashboardAccess.
|
||||||
func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) {
|
func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) {
|
||||||
created := false
|
created := false
|
||||||
user, err := identity.GetRequester(ctx)
|
user, ok := claims.From(ctx)
|
||||||
if err != nil {
|
if !ok || user == nil {
|
||||||
return nil, created, err
|
return nil, created, fmt.Errorf("no user found in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
if dash.Name != "" {
|
if dash.Name != "" {
|
||||||
dash.Spec.Set("uid", dash.Name)
|
dash.Spec.Set("uid", dash.Name)
|
||||||
|
|
||||||
@@ -371,8 +373,10 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userID int64
|
var userID int64
|
||||||
if user.IsIdentityType(claims.TypeUser) {
|
idClaims := user.GetIdentity()
|
||||||
userID, err = user.GetInternalID()
|
if claims.IsIdentityType(idClaims.IdentityType(), claims.TypeUser) {
|
||||||
|
var err error
|
||||||
|
userID, err = identity.UserIdentifier(idClaims.Subject())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|||||||
54
pkg/services/auth/idtest/internal_token.go
Normal file
54
pkg/services/auth/idtest/internal_token.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package idtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v3"
|
||||||
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateInternalToken(authInfo claims.AuthInfo, secret []byte) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
|
||||||
|
signerOpts := jose.SignerOptions{}
|
||||||
|
signerOpts.WithType("jwt") // Should be uppercase, but this is what authlib expects
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: secret}, &signerOpts)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
identity := authInfo.GetIdentity()
|
||||||
|
now := time.Now()
|
||||||
|
tokenTTL := 10 * time.Minute
|
||||||
|
idClaims := &auth.IDClaims{
|
||||||
|
Claims: &jwt.Claims{
|
||||||
|
Audience: identity.Audience(),
|
||||||
|
Subject: identity.Subject(),
|
||||||
|
Expiry: jwt.NewNumericDate(now.Add(tokenTTL)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
},
|
||||||
|
Rest: authnlib.IDTokenClaims{
|
||||||
|
Namespace: identity.Namespace(),
|
||||||
|
Identifier: identity.Identifier(),
|
||||||
|
Type: identity.IdentityType(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.IsIdentityType(identity.IdentityType(), claims.TypeUser) {
|
||||||
|
idClaims.Rest.Email = identity.Email()
|
||||||
|
idClaims.Rest.EmailVerified = identity.EmailVerified()
|
||||||
|
idClaims.Rest.AuthenticatedBy = identity.AuthenticatedBy()
|
||||||
|
idClaims.Rest.Username = identity.Username()
|
||||||
|
idClaims.Rest.DisplayName = identity.DisplayName()
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := jwt.Signed(signer).Claims(&idClaims.Rest).Claims(idClaims.Claims)
|
||||||
|
token, err := builder.CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, idClaims, nil
|
||||||
|
}
|
||||||
14
pkg/services/authn/grpcutils/grpc_authenticator.go
Normal file
14
pkg/services/authn/grpcutils/grpc_authenticator.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package grpcutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewInProcGrpcAuthenticator() *authnlib.GrpcAuthenticator {
|
||||||
|
// In proc grpc ID token signature verification can be skipped
|
||||||
|
return authnlib.NewUnsafeGrpcAuthenticator(
|
||||||
|
&authnlib.GrpcAuthenticatorConfig{},
|
||||||
|
authnlib.WithDisableAccessTokenAuthOption(),
|
||||||
|
authnlib.WithIDTokenAuthOption(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fullstorydev/grpchan"
|
"github.com/fullstorydev/grpchan"
|
||||||
"github.com/fullstorydev/grpchan/inprocgrpc"
|
"github.com/fullstorydev/grpchan/inprocgrpc"
|
||||||
|
"github.com/go-jose/go-jose/v3"
|
||||||
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
|
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
|
||||||
grpcUtils "github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
|
grpcUtils "github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +45,7 @@ func NewResourceClient(channel *grpc.ClientConn) ResourceClient {
|
|||||||
func NewLocalResourceClient(server ResourceServer) ResourceClient {
|
func NewLocalResourceClient(server ResourceServer) ResourceClient {
|
||||||
channel := &inprocgrpc.Channel{}
|
channel := &inprocgrpc.Channel{}
|
||||||
|
|
||||||
auth := &grpcUtils.Authenticator{}
|
grpcAuthInt := grpcutils.NewInProcGrpcAuthenticator()
|
||||||
for _, desc := range []*grpc.ServiceDesc{
|
for _, desc := range []*grpc.ServiceDesc{
|
||||||
&ResourceStore_ServiceDesc,
|
&ResourceStore_ServiceDesc,
|
||||||
&ResourceIndex_ServiceDesc,
|
&ResourceIndex_ServiceDesc,
|
||||||
@@ -43,17 +54,93 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
|
|||||||
channel.RegisterService(
|
channel.RegisterService(
|
||||||
grpchan.InterceptServer(
|
grpchan.InterceptServer(
|
||||||
desc,
|
desc,
|
||||||
grpcAuth.UnaryServerInterceptor(auth.Authenticate),
|
grpcAuth.UnaryServerInterceptor(grpcAuthInt.Authenticate),
|
||||||
grpcAuth.StreamServerInterceptor(auth.Authenticate),
|
grpcAuth.StreamServerInterceptor(grpcAuthInt.Authenticate),
|
||||||
),
|
),
|
||||||
server,
|
server,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cc := grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor)
|
clientInt, _ := authnlib.NewGrpcClientInterceptor(
|
||||||
|
&authnlib.GrpcClientConfig{},
|
||||||
|
authnlib.WithDisableAccessTokenOption(),
|
||||||
|
authnlib.WithIDTokenExtractorOption(idTokenExtractor),
|
||||||
|
)
|
||||||
|
|
||||||
|
cc := grpchan.InterceptClientConn(channel, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor)
|
||||||
return &resourceClient{
|
return &resourceClient{
|
||||||
ResourceStoreClient: NewResourceStoreClient(cc),
|
ResourceStoreClient: NewResourceStoreClient(cc),
|
||||||
ResourceIndexClient: NewResourceIndexClient(cc),
|
ResourceIndexClient: NewResourceIndexClient(cc),
|
||||||
DiagnosticsClient: NewDiagnosticsClient(cc),
|
DiagnosticsClient: NewDiagnosticsClient(cc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func idTokenExtractor(ctx context.Context) (string, error) {
|
||||||
|
authInfo, ok := claims.From(ctx)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("no claims found")
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := authInfo.GetExtra()
|
||||||
|
if token, exists := extra["id-token"]; exists && len(token) != 0 && token[0] != "" {
|
||||||
|
return token[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no token is found, create an internal token.
|
||||||
|
// This is a workaround for StaticRequester not having a signed ID token.
|
||||||
|
if staticRequester, ok := authInfo.(*identity.StaticRequester); ok {
|
||||||
|
token, idClaims, err := createInternalToken(staticRequester)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create internal token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
staticRequester.IDToken = token
|
||||||
|
staticRequester.IDTokenClaims = idClaims
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("id-token not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createInternalToken creates a symmetrically signed token for using in in-proc mode only.
|
||||||
|
func createInternalToken(authInfo claims.AuthInfo) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
|
||||||
|
signerOpts := jose.SignerOptions{}
|
||||||
|
signerOpts.WithType("jwt") // Should be uppercase, but this is what authlib expects
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: []byte("internal key")}, &signerOpts)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
identity := authInfo.GetIdentity()
|
||||||
|
now := time.Now()
|
||||||
|
tokenTTL := 10 * time.Minute
|
||||||
|
idClaims := &auth.IDClaims{
|
||||||
|
Claims: &jwt.Claims{
|
||||||
|
Audience: identity.Audience(),
|
||||||
|
Subject: identity.Subject(),
|
||||||
|
Expiry: jwt.NewNumericDate(now.Add(tokenTTL)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
},
|
||||||
|
Rest: authnlib.IDTokenClaims{
|
||||||
|
Namespace: identity.Namespace(),
|
||||||
|
Identifier: identity.Identifier(),
|
||||||
|
Type: identity.IdentityType(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.IsIdentityType(identity.IdentityType(), claims.TypeUser) {
|
||||||
|
idClaims.Rest.Email = identity.Email()
|
||||||
|
idClaims.Rest.EmailVerified = identity.EmailVerified()
|
||||||
|
idClaims.Rest.AuthenticatedBy = identity.AuthenticatedBy()
|
||||||
|
idClaims.Rest.Username = identity.Username()
|
||||||
|
idClaims.Rest.DisplayName = identity.DisplayName()
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := jwt.Signed(signer).Claims(&idClaims.Rest).Claims(idClaims.Claims)
|
||||||
|
token, err := builder.CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, idClaims, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth/idtest"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
@@ -509,6 +510,11 @@ func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.Ro
|
|||||||
require.Equal(c.t, orgId, s.OrgID)
|
require.Equal(c.t, orgId, s.OrgID)
|
||||||
require.Equal(c.t, basicRole, s.OrgRole) // make sure the role was set properly
|
require.Equal(c.t, basicRole, s.OrgRole) // make sure the role was set properly
|
||||||
|
|
||||||
|
idToken, idClaims, err := idtest.CreateInternalToken(s, []byte("secret"))
|
||||||
|
require.NoError(c.t, err)
|
||||||
|
s.IDToken = idToken
|
||||||
|
s.IDTokenClaims = idClaims
|
||||||
|
|
||||||
usr := User{
|
usr := User{
|
||||||
Identity: s,
|
Identity: s,
|
||||||
password: name,
|
password: name,
|
||||||
|
|||||||
Reference in New Issue
Block a user