GRPC Server: Add signedInUser to context (#56059)

This commit is contained in:
Todd Treece 2022-10-04 16:31:02 -04:00 committed by GitHub
parent ba97b268d0
commit 4163f31d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 182 additions and 29 deletions

View File

@ -71,6 +71,8 @@ import (
"github.com/grafana/grafana/pkg/services/export"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grpcserver"
grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements"
@ -344,9 +346,11 @@ var wireBasicSet = wire.NewSet(
publicdashboardsApi.ProvideApi,
userimpl.ProvideService,
orgimpl.ProvideService,
grpccontext.ProvideContextHandler,
grpcserver.ProvideService,
grpcserver.ProvideHealthService,
grpcserver.ProvideReflectionService,
interceptors.ProvideAuthenticator,
objectdummyserver.ProvideDummyObjectServer,
object.ProvideHTTPObjectStore,
teamimpl.ProvideService,

View File

@ -0,0 +1,25 @@
package grpccontext
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/user"
)
type grpcContextKey struct{}
type GRPCServerContext struct {
SignedInUser *user.SignedInUser
Tracer tracing.Tracer
Logger log.Logger
}
func FromContext(ctx context.Context) *GRPCServerContext {
grpcContext, ok := ctx.Value(grpcContextKey{}).(*GRPCServerContext)
if !ok {
return nil
}
return grpcContext
}

View File

@ -0,0 +1,47 @@
package grpccontext
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/user"
)
type ContextHandler interface {
SetUser(context.Context, *user.SignedInUser) context.Context
GetUser(context.Context) *user.SignedInUser
}
func ProvideContextHandler(tracer tracing.Tracer) ContextHandler {
return &contextHandler{
tracer: tracer,
}
}
type contextHandler struct {
tracer tracing.Tracer
}
func (c *contextHandler) fromContext(ctx context.Context) *GRPCServerContext {
grpcContext := FromContext(ctx)
if grpcContext != nil {
return grpcContext
}
return &GRPCServerContext{
Tracer: c.tracer,
Logger: log.New("grpc-server-context"),
}
}
func (c *contextHandler) SetUser(ctx context.Context, user *user.SignedInUser) context.Context {
grpcContext := c.fromContext(ctx)
grpcContext.SignedInUser = user
return context.WithValue(ctx, grpcContextKey{}, grpcContext)
}
func (c *contextHandler) GetUser(ctx context.Context) *user.SignedInUser {
return c.fromContext(ctx).SignedInUser
}

View File

@ -2,13 +2,14 @@ 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/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
@ -17,30 +18,40 @@ import (
"google.golang.org/grpc/status"
)
// Authenticator can authenticate GRPC requests.
type Authenticator struct {
logger log.Logger
APIKey apikey.Service
UserService user.Service
type Authenticator interface {
Authenticate(ctx context.Context) (context.Context, error)
}
func NewAuthenticator(apiKey apikey.Service, userService user.Service) *Authenticator {
return &Authenticator{
logger: log.New("grpc-server-authenticator"),
APIKey: apiKey,
UserService: userService,
// authenticator can authenticate GRPC requests.
type authenticator struct {
contextHandler grpccontext.ContextHandler
logger log.Logger
APIKeyService apikey.Service
UserService user.Service
AccessControlService accesscontrol.Service
}
func ProvideAuthenticator(apiKeyService apikey.Service, userService user.Service, accessControlService accesscontrol.Service, contextHandler grpccontext.ContextHandler) Authenticator {
return &authenticator{
contextHandler: contextHandler,
logger: log.New("grpc-server-authenticator"),
AccessControlService: accessControlService,
APIKeyService: apiKeyService,
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) {
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) {
func (a *authenticator) tokenAuth(ctx context.Context) (context.Context, error) {
auth, err := extractAuthorization(ctx)
if err != nil {
return ctx, err
@ -57,16 +68,18 @@ func (a *Authenticator) tokenAuth(ctx context.Context) (context.Context, error)
newCtx := purgeHeader(ctx, "authorization")
_, err = a.getSignedInUser(ctx, token)
signedInUser, 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")
}
newCtx = a.contextHandler.SetUser(newCtx, signedInUser)
return newCtx, nil
}
func (a *Authenticator) getSignedInUser(ctx context.Context, token string) (*user.SignedInUser, error) {
func (a *authenticator) getSignedInUser(ctx context.Context, token string) (*user.SignedInUser, error) {
decoded, err := apikeygenprefix.Decode(token)
if err != nil {
return nil, err
@ -77,7 +90,7 @@ func (a *Authenticator) getSignedInUser(ctx context.Context, token string) (*use
return nil, err
}
apikey, err := a.APIKey.GetAPIKeyByHash(ctx, hash)
apikey, err := a.APIKeyService.GetAPIKeyByHash(ctx, hash)
if err != nil {
return nil, err
}
@ -92,13 +105,29 @@ func (a *Authenticator) getSignedInUser(ctx context.Context, token string) (*use
return nil, err
}
if signedInUser == nil {
return nil, status.Error(codes.Unauthenticated, "service account not found")
}
if !signedInUser.HasRole(org.RoleAdmin) {
return nil, fmt.Errorf("api key does not have admin role")
return nil, status.Error(codes.PermissionDenied, "service account 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 nil, status.Error(codes.PermissionDenied, "service account is disabled")
}
if signedInUser.Permissions == nil {
signedInUser.Permissions = make(map[int64]map[string][]string)
}
if signedInUser.Permissions[signedInUser.OrgID] == nil {
permissions, err := a.AccessControlService.GetUserPermissions(ctx, signedInUser, accesscontrol.Options{})
if err != nil {
a.logger.Error("failed fetching permissions for user", "userID", signedInUser.UserID, "error", err)
}
signedInUser.Permissions[signedInUser.OrgID] = accesscontrol.GroupScopesByAction(permissions)
}
return signedInUser, nil

View File

@ -5,7 +5,11 @@ import (
"testing"
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apikey"
grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
@ -13,6 +17,7 @@ import (
)
func TestAuthenticator_Authenticate(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
serviceAccountId := int64(1)
t.Run("accepts service api key with admin role", func(t *testing.T) {
s := newFakeAPIKey(&apikey.APIKey{
@ -22,7 +27,8 @@ func TestAuthenticator_Authenticate(t *testing.T) {
Name: "Admin API Key",
ServiceAccountId: &serviceAccountId,
}, nil)
a := NewAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin})
ac := accesscontrolmock.New()
a := ProvideAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin}, ac, grpccontext.ProvideContextHandler(tracer))
ctx, err := setupContext()
require.NoError(t, err)
_, err = a.Authenticate(ctx)
@ -37,7 +43,8 @@ func TestAuthenticator_Authenticate(t *testing.T) {
Name: "Admin API Key",
ServiceAccountId: &serviceAccountId,
}, nil)
a := NewAuthenticator(s, &fakeUserService{OrgRole: org.RoleEditor})
ac := accesscontrolmock.New()
a := ProvideAuthenticator(s, &fakeUserService{OrgRole: org.RoleEditor}, ac, grpccontext.ProvideContextHandler(tracer))
ctx, err := setupContext()
require.NoError(t, err)
_, err = a.Authenticate(ctx)
@ -52,7 +59,8 @@ func TestAuthenticator_Authenticate(t *testing.T) {
Name: "Admin API Key",
ServiceAccountId: &serviceAccountId,
}, nil)
a := NewAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin})
ac := accesscontrolmock.New()
a := ProvideAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin}, ac, grpccontext.ProvideContextHandler(tracer))
ctx, err := setupContext()
require.NoError(t, err)
md, ok := metadata.FromIncomingContext(ctx)
@ -64,6 +72,49 @@ func TestAuthenticator_Authenticate(t *testing.T) {
require.True(t, ok)
require.Empty(t, md["authorization"])
})
t.Run("sets SignInUser", func(t *testing.T) {
s := newFakeAPIKey(&apikey.APIKey{
Id: 1,
OrgId: 1,
Key: "admin-api-key",
Name: "Admin API Key",
ServiceAccountId: &serviceAccountId,
}, nil)
ac := accesscontrolmock.New()
a := ProvideAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin}, ac, grpccontext.ProvideContextHandler(tracer))
ctx, err := setupContext()
require.NoError(t, err)
ctx, err = a.Authenticate(ctx)
require.NoError(t, err)
signedInUser := grpccontext.FromContext(ctx).SignedInUser
require.Equal(t, serviceAccountId, signedInUser.UserID)
})
t.Run("sets SignInUser permissions", func(t *testing.T) {
s := newFakeAPIKey(&apikey.APIKey{
Id: 1,
OrgId: 1,
Key: "admin-api-key",
Name: "Admin API Key",
ServiceAccountId: &serviceAccountId,
}, nil)
permissions := []accesscontrol.Permission{
{
Action: accesscontrol.ActionAPIKeyRead,
Scope: accesscontrol.ScopeAPIKeysAll,
},
}
ac := accesscontrolmock.New().WithPermissions(permissions)
a := ProvideAuthenticator(s, &fakeUserService{OrgRole: org.RoleAdmin}, ac, grpccontext.ProvideContextHandler(tracer))
ctx, err := setupContext()
require.NoError(t, err)
ctx, err = a.Authenticate(ctx)
require.NoError(t, err)
signedInUser := grpccontext.FromContext(ctx).SignedInUser
require.Equal(t, serviceAccountId, signedInUser.UserID)
require.Equal(t, []string{accesscontrol.ScopeAPIKeysAll}, signedInUser.Permissions[1][accesscontrol.ActionAPIKeyRead])
})
}
type fakeAPIKey struct {
@ -90,9 +141,10 @@ type fakeUserService struct {
func (f *fakeUserService) GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
return &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: f.OrgRole,
UserID: 1,
OrgID: 1,
OrgRole: f.OrgRole,
Permissions: make(map[int64]map[string][]string),
}, nil
}

View File

@ -9,10 +9,8 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
@ -34,7 +32,7 @@ type GPRCServerService struct {
address string
}
func ProvideService(cfg *setting.Cfg, apiKey apikey.Service, userService user.Service, tracer tracing.Tracer) (Provider, error) {
func ProvideService(cfg *setting.Cfg, authenticator interceptors.Authenticator, tracer tracing.Tracer) (Provider, error) {
s := &GPRCServerService{
cfg: cfg,
logger: log.New("grpc-server"),
@ -45,8 +43,6 @@ func ProvideService(cfg *setting.Cfg, apiKey apikey.Service, userService user.Se
// Default auth is admin token check, but this can be overridden by
// services which implement ServiceAuthFuncOverride interface.
// See https://github.com/grpc-ecosystem/go-grpc-middleware/blob/master/auth/auth.go#L30.
authenticator := interceptors.NewAuthenticator(apiKey, userService)
opts = append(opts, []grpc.ServerOption{
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(