From 4163f31d7613df6b1a9e1e46d7ea57602d13c945 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Tue, 4 Oct 2022 16:31:02 -0400 Subject: [PATCH] GRPC Server: Add signedInUser to context (#56059) --- pkg/server/wire.go | 4 ++ pkg/services/grpcserver/context/context.go | 25 +++++++ pkg/services/grpcserver/context/handler.go | 47 ++++++++++++++ pkg/services/grpcserver/interceptors/auth.go | 65 ++++++++++++++----- .../grpcserver/interceptors/auth_test.go | 64 ++++++++++++++++-- pkg/services/grpcserver/service.go | 6 +- 6 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 pkg/services/grpcserver/context/context.go create mode 100644 pkg/services/grpcserver/context/handler.go diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 6f08394792f..b7d9768b756 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -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, diff --git a/pkg/services/grpcserver/context/context.go b/pkg/services/grpcserver/context/context.go new file mode 100644 index 00000000000..5cec862ca1c --- /dev/null +++ b/pkg/services/grpcserver/context/context.go @@ -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 +} diff --git a/pkg/services/grpcserver/context/handler.go b/pkg/services/grpcserver/context/handler.go new file mode 100644 index 00000000000..75cc5257d13 --- /dev/null +++ b/pkg/services/grpcserver/context/handler.go @@ -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 +} diff --git a/pkg/services/grpcserver/interceptors/auth.go b/pkg/services/grpcserver/interceptors/auth.go index 452ef615383..e3ab8843e80 100644 --- a/pkg/services/grpcserver/interceptors/auth.go +++ b/pkg/services/grpcserver/interceptors/auth.go @@ -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 diff --git a/pkg/services/grpcserver/interceptors/auth_test.go b/pkg/services/grpcserver/interceptors/auth_test.go index 353e785c44c..bc1dacd5541 100644 --- a/pkg/services/grpcserver/interceptors/auth_test.go +++ b/pkg/services/grpcserver/interceptors/auth_test.go @@ -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 } diff --git a/pkg/services/grpcserver/service.go b/pkg/services/grpcserver/service.go index d23b20695ec..c7349a48954 100644 --- a/pkg/services/grpcserver/service.go +++ b/pkg/services/grpcserver/service.go @@ -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(