GRPC Server: Add tracing interceptors (#56045)
Co-authored-by: Artur Wierzbicki <artur.wierzbicki@grafana.com>
This commit is contained in:
co-authored by
Artur Wierzbicki
parent
a863a4d95d
commit
2d433194d0
@@ -0,0 +1,130 @@
|
||||
package interceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Authenticator can authenticate GRPC requests.
|
||||
type Authenticator struct {
|
||||
logger log.Logger
|
||||
APIKey apikey.Service
|
||||
UserService user.Service
|
||||
}
|
||||
|
||||
func NewAuthenticator(apiKey apikey.Service, userService user.Service) *Authenticator {
|
||||
return &Authenticator{
|
||||
logger: log.New("grpc-server-authenticator"),
|
||||
APIKey: apiKey,
|
||||
UserService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate checks that a token exists and is valid, and then removes the token from the
|
||||
// authorization header in the context.
|
||||
func (a *Authenticator) Authenticate(ctx context.Context) (context.Context, error) {
|
||||
return a.tokenAuth(ctx)
|
||||
}
|
||||
|
||||
const tokenPrefix = "Bearer "
|
||||
|
||||
func (a *Authenticator) tokenAuth(ctx context.Context) (context.Context, error) {
|
||||
auth, err := extractAuthorization(ctx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, tokenPrefix) {
|
||||
return ctx, status.Error(codes.Unauthenticated, `missing "Bearer " prefix in "authorization" value`)
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(auth, tokenPrefix)
|
||||
if token == "" {
|
||||
return ctx, status.Error(codes.Unauthenticated, "token required")
|
||||
}
|
||||
|
||||
newCtx := purgeHeader(ctx, "authorization")
|
||||
|
||||
_, err = a.getSignedInUser(ctx, token)
|
||||
if err != nil {
|
||||
logger.Warn("request with invalid token", "error", err, "token", token)
|
||||
return ctx, status.Error(codes.Unauthenticated, "invalid token")
|
||||
}
|
||||
|
||||
return newCtx, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getSignedInUser(ctx context.Context, token string) (*user.SignedInUser, error) {
|
||||
decoded, err := apikeygenprefix.Decode(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := decoded.Hash()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apikey, err := a.APIKey.GetAPIKeyByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if apikey == nil || apikey.ServiceAccountId == nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "api key does not have a service account")
|
||||
}
|
||||
|
||||
querySignedInUser := user.GetSignedInUserQuery{UserID: *apikey.ServiceAccountId, OrgID: apikey.OrgId}
|
||||
signedInUser, err := a.UserService.GetSignedInUserWithCacheCtx(ctx, &querySignedInUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !signedInUser.HasRole(org.RoleAdmin) {
|
||||
return nil, fmt.Errorf("api key does not have admin role")
|
||||
}
|
||||
|
||||
// disabled service accounts are not allowed to access the API
|
||||
if signedInUser.IsDisabled {
|
||||
return nil, fmt.Errorf("service account is disabled")
|
||||
}
|
||||
|
||||
return signedInUser, nil
|
||||
}
|
||||
|
||||
func extractAuthorization(ctx context.Context) (string, error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return "", status.Error(codes.Unauthenticated, "no headers in request")
|
||||
}
|
||||
|
||||
authHeaders, ok := md["authorization"]
|
||||
if !ok {
|
||||
return "", status.Error(codes.Unauthenticated, `no "authorization" header in request`)
|
||||
}
|
||||
|
||||
if len(authHeaders) != 1 {
|
||||
return "", status.Error(codes.Unauthenticated, `malformed "authorization" header: one value required`)
|
||||
}
|
||||
|
||||
return authHeaders[0], nil
|
||||
}
|
||||
|
||||
func purgeHeader(ctx context.Context, header string) context.Context {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
mdCopy := md.Copy()
|
||||
mdCopy[header] = nil
|
||||
return metadata.NewIncomingContext(ctx, mdCopy)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package interceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestAuthenticator_Authenticate(t *testing.T) {
|
||||
serviceAccountId := int64(1)
|
||||
t.Run("accepts service api key with admin role", func(t *testing.T) {
|
||||
s := newFakeAPIKey(&apikey.APIKey{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
Key: "admin-api-key",
|
||||
Name: "Admin API Key",
|
||||
ServiceAccountId: &serviceAccountId,
|
||||
}, nil)
|
||||
a := NewAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin})
|
||||
ctx, err := setupContext()
|
||||
require.NoError(t, err)
|
||||
_, err = a.Authenticate(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("rejects non-admin role", func(t *testing.T) {
|
||||
s := newFakeAPIKey(&apikey.APIKey{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
Key: "admin-api-key",
|
||||
Name: "Admin API Key",
|
||||
ServiceAccountId: &serviceAccountId,
|
||||
}, nil)
|
||||
a := NewAuthenticator(s, &fakeUserService{OrgRole: org.RoleEditor})
|
||||
ctx, err := setupContext()
|
||||
require.NoError(t, err)
|
||||
_, err = a.Authenticate(ctx)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("removes auth header from context", func(t *testing.T) {
|
||||
s := newFakeAPIKey(&apikey.APIKey{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
Key: "admin-api-key",
|
||||
Name: "Admin API Key",
|
||||
ServiceAccountId: &serviceAccountId,
|
||||
}, nil)
|
||||
a := NewAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin})
|
||||
ctx, err := setupContext()
|
||||
require.NoError(t, err)
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, md["authorization"])
|
||||
ctx, err = a.Authenticate(ctx)
|
||||
require.NoError(t, err)
|
||||
md, ok = metadata.FromIncomingContext(ctx)
|
||||
require.True(t, ok)
|
||||
require.Empty(t, md["authorization"])
|
||||
})
|
||||
}
|
||||
|
||||
type fakeAPIKey struct {
|
||||
apikey.Service
|
||||
key *apikey.APIKey
|
||||
err error
|
||||
}
|
||||
|
||||
func newFakeAPIKey(key *apikey.APIKey, err error) *fakeAPIKey {
|
||||
return &fakeAPIKey{
|
||||
key: key,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeAPIKey) GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error) {
|
||||
return f.key, f.err
|
||||
}
|
||||
|
||||
type fakeUserService struct {
|
||||
user.Service
|
||||
OrgRole org.RoleType
|
||||
}
|
||||
|
||||
func (f *fakeUserService) GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
|
||||
return &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: f.OrgRole,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setupContext() (context.Context, error) {
|
||||
ctx := context.Background()
|
||||
key, err := apikeygenprefix.New("sa")
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
md := metadata.New(map[string]string{})
|
||||
md["authorization"] = []string{"Bearer " + key.ClientSecret}
|
||||
return metadata.NewIncomingContext(ctx, md), nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package interceptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const tracingPrefix = "gRPC Server "
|
||||
|
||||
func TracingUnaryInterceptor(tracer tracing.Tracer) grpc.UnaryServerInterceptor {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
req interface{},
|
||||
info *grpc.UnaryServerInfo,
|
||||
handler grpc.UnaryHandler,
|
||||
) (resp interface{}, err error) {
|
||||
ctx, span := tracer.Start(ctx, tracingPrefix+info.FullMethod)
|
||||
defer span.End()
|
||||
resp, err = handler(ctx, req)
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
func TracingStreamInterceptor(tracer tracing.Tracer) grpc.StreamServerInterceptor {
|
||||
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
ctx, span := tracer.Start(stream.Context(), tracingPrefix+info.FullMethod)
|
||||
defer span.End()
|
||||
tracingStream := &tracingServerStream{
|
||||
ServerStream: stream,
|
||||
ctx: ctx,
|
||||
}
|
||||
return handler(srv, tracingStream)
|
||||
}
|
||||
}
|
||||
|
||||
type tracingServerStream struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (s *tracingServerStream) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
Reference in New Issue
Block a user