mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GRPC Server: Add signedInUser to context (#56059)
This commit is contained in:
parent
ba97b268d0
commit
4163f31d76
@ -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,
|
||||
|
25
pkg/services/grpcserver/context/context.go
Normal file
25
pkg/services/grpcserver/context/context.go
Normal 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
|
||||
}
|
47
pkg/services/grpcserver/context/handler.go
Normal file
47
pkg/services/grpcserver/context/handler.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user