mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GRPC Server: Add gRPC server service (#47849)
Co-authored-by: Todd Treece <todd.treece@grafana.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
f11495a4c3
commit
55aae79744
@ -64,6 +64,7 @@ export interface FeatureToggles {
|
|||||||
internationalization?: boolean;
|
internationalization?: boolean;
|
||||||
topnav?: boolean;
|
topnav?: boolean;
|
||||||
customBranding?: boolean;
|
customBranding?: boolean;
|
||||||
|
grpcServer?: boolean;
|
||||||
traceqlEditor?: boolean;
|
traceqlEditor?: boolean;
|
||||||
redshiftAsyncQueryDataSupport?: boolean;
|
redshiftAsyncQueryDataSupport?: boolean;
|
||||||
athenaAsyncQueryDataSupport?: boolean;
|
athenaAsyncQueryDataSupport?: boolean;
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||||
|
"github.com/grafana/grafana/pkg/services/grpcserver"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
"github.com/grafana/grafana/pkg/services/live"
|
||||||
"github.com/grafana/grafana/pkg/services/live/pushhttp"
|
"github.com/grafana/grafana/pkg/services/live/pushhttp"
|
||||||
@ -43,11 +44,13 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
|
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
|
||||||
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
|
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
|
||||||
saService *samanager.ServiceAccountsService, authInfoService *authinfoservice.Implementation,
|
saService *samanager.ServiceAccountsService, authInfoService *authinfoservice.Implementation,
|
||||||
|
grpcServerProvider grpcserver.Provider,
|
||||||
secretMigrationProvider secretsMigrations.SecretMigrationProvider,
|
secretMigrationProvider secretsMigrations.SecretMigrationProvider,
|
||||||
// Need to make sure these are initialized, is there a better place to put them?
|
// Need to make sure these are initialized, is there a better place to put them?
|
||||||
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||||
_ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider,
|
_ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider,
|
||||||
|
_ *grpcserver.HealthService,
|
||||||
) *BackgroundServiceRegistry {
|
) *BackgroundServiceRegistry {
|
||||||
return NewBackgroundServiceRegistry(
|
return NewBackgroundServiceRegistry(
|
||||||
httpServer,
|
httpServer,
|
||||||
@ -72,6 +75,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
thumbnailsService,
|
thumbnailsService,
|
||||||
searchService,
|
searchService,
|
||||||
entityEventsService,
|
entityEventsService,
|
||||||
|
grpcServerProvider,
|
||||||
saService,
|
saService,
|
||||||
authInfoService,
|
authInfoService,
|
||||||
processManager,
|
processManager,
|
||||||
|
@ -75,6 +75,7 @@ import (
|
|||||||
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
||||||
"github.com/grafana/grafana/pkg/services/export"
|
"github.com/grafana/grafana/pkg/services/export"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/grpcserver"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
@ -342,16 +343,18 @@ var wireBasicSet = wire.NewSet(
|
|||||||
publicdashboardsApi.ProvideApi,
|
publicdashboardsApi.ProvideApi,
|
||||||
userimpl.ProvideService,
|
userimpl.ProvideService,
|
||||||
orgimpl.ProvideService,
|
orgimpl.ProvideService,
|
||||||
|
grpcserver.ProvideService,
|
||||||
|
grpcserver.ProvideHealthService,
|
||||||
teamimpl.ProvideService,
|
teamimpl.ProvideService,
|
||||||
tempuserimpl.ProvideService,
|
tempuserimpl.ProvideService,
|
||||||
dashboardthumbsimpl.ProvideService,
|
dashboardthumbsimpl.ProvideService,
|
||||||
loginattemptimpl.ProvideService,
|
loginattemptimpl.ProvideService,
|
||||||
|
userauthimpl.ProvideService,
|
||||||
secretsMigrations.ProvideDataSourceMigrationService,
|
secretsMigrations.ProvideDataSourceMigrationService,
|
||||||
secretsMigrations.ProvideMigrateToPluginService,
|
secretsMigrations.ProvideMigrateToPluginService,
|
||||||
secretsMigrations.ProvideMigrateFromPluginService,
|
secretsMigrations.ProvideMigrateFromPluginService,
|
||||||
secretsMigrations.ProvideSecretMigrationProvider,
|
secretsMigrations.ProvideSecretMigrationProvider,
|
||||||
wire.Bind(new(secretsMigrations.SecretMigrationProvider), new(*secretsMigrations.SecretMigrationProviderImpl)),
|
wire.Bind(new(secretsMigrations.SecretMigrationProvider), new(*secretsMigrations.SecretMigrationProviderImpl)),
|
||||||
userauthimpl.ProvideService,
|
|
||||||
acimpl.ProvideAccessControl,
|
acimpl.ProvideAccessControl,
|
||||||
navtreeimpl.ProvideService,
|
navtreeimpl.ProvideService,
|
||||||
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),
|
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),
|
||||||
|
@ -268,6 +268,11 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Name: "grpcServer",
|
||||||
|
Description: "Run GRPC server",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
RequiresDevMode: true,
|
||||||
|
}, {
|
||||||
Name: "traceqlEditor",
|
Name: "traceqlEditor",
|
||||||
Description: "Show the TraceQL editor in the explore page",
|
Description: "Show the TraceQL editor in the explore page",
|
||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
|
@ -199,6 +199,10 @@ const (
|
|||||||
// Replaces whitelabeling with the new custom branding feature
|
// Replaces whitelabeling with the new custom branding feature
|
||||||
FlagCustomBranding = "customBranding"
|
FlagCustomBranding = "customBranding"
|
||||||
|
|
||||||
|
// FlagGrpcServer
|
||||||
|
// Run GRPC server
|
||||||
|
FlagGrpcServer = "grpcServer"
|
||||||
|
|
||||||
// FlagTraceqlEditor
|
// FlagTraceqlEditor
|
||||||
// Show the TraceQL editor in the explore page
|
// Show the TraceQL editor in the explore page
|
||||||
FlagTraceqlEditor = "traceqlEditor"
|
FlagTraceqlEditor = "traceqlEditor"
|
||||||
|
129
pkg/services/grpcserver/auth.go
Normal file
129
pkg/services/grpcserver/auth.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
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.validateToken(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) validateToken(ctx context.Context, keyString string) error {
|
||||||
|
decoded, err := apikeygenprefix.Decode(keyString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := decoded.Hash()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apikey, err := a.APIKey.GetAPIKeyByHash(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if apikey == nil || apikey.ServiceAccountId == nil {
|
||||||
|
return 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 err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !signedInUser.HasRole(org.RoleAdmin) {
|
||||||
|
return fmt.Errorf("api key does not have admin role")
|
||||||
|
}
|
||||||
|
|
||||||
|
// disabled service accounts are not allowed to access the API
|
||||||
|
if signedInUser.IsDisabled {
|
||||||
|
return fmt.Errorf("service account is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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)
|
||||||
|
}
|
108
pkg/services/grpcserver/auth_test.go
Normal file
108
pkg/services/grpcserver/auth_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
37
pkg/services/grpcserver/health.go
Normal file
37
pkg/services/grpcserver/health.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/health"
|
||||||
|
"google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HealthService implements GRPC Health Checking Protocol:
|
||||||
|
// https://github.com/grpc/grpc/blob/master/doc/health-checking.md
|
||||||
|
// It also demonstrates how to override authentication for a service – in this
|
||||||
|
// case we are disabling any auth in AuthFuncOverride.
|
||||||
|
type HealthService struct {
|
||||||
|
cfg *setting.Cfg
|
||||||
|
healthServer *healthServer
|
||||||
|
}
|
||||||
|
|
||||||
|
type healthServer struct {
|
||||||
|
*health.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthFuncOverride for no auth for health service.
|
||||||
|
func (s *healthServer) AuthFuncOverride(ctx context.Context, _ string) (context.Context, error) {
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProvideHealthService(cfg *setting.Cfg, grpcServerProvider Provider) (*HealthService, error) {
|
||||||
|
hs := &healthServer{health.NewServer()}
|
||||||
|
grpc_health_v1.RegisterHealthServer(grpcServerProvider.GetServer(), hs)
|
||||||
|
return &HealthService{
|
||||||
|
cfg: cfg,
|
||||||
|
healthServer: hs,
|
||||||
|
}, nil
|
||||||
|
}
|
98
pkg/services/grpcserver/service.go
Normal file
98
pkg/services/grpcserver/service.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"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/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/reflection"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
registry.BackgroundService
|
||||||
|
GetServer() *grpc.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
type GPRCServerService struct {
|
||||||
|
cfg *setting.Cfg
|
||||||
|
logger log.Logger
|
||||||
|
server *grpc.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProvideService(cfg *setting.Cfg, apiKey apikey.Service, userService user.Service) (Provider, error) {
|
||||||
|
s := &GPRCServerService{
|
||||||
|
cfg: cfg,
|
||||||
|
logger: log.New("grpc-server"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []grpc.ServerOption
|
||||||
|
|
||||||
|
// 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 := newAuthenticator(apiKey, userService)
|
||||||
|
opts = append(opts, []grpc.ServerOption{
|
||||||
|
grpc.StreamInterceptor(grpcAuth.StreamServerInterceptor(authenticator.authenticate)),
|
||||||
|
grpc.UnaryInterceptor(grpcAuth.UnaryServerInterceptor(authenticator.authenticate)),
|
||||||
|
}...)
|
||||||
|
|
||||||
|
if s.cfg.GRPCServerTLSConfig != nil {
|
||||||
|
opts = append(opts, grpc.Creds(credentials.NewTLS(cfg.GRPCServerTLSConfig)))
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer(opts...)
|
||||||
|
reflection.Register(grpcServer)
|
||||||
|
s.server = grpcServer
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GPRCServerService) Run(ctx context.Context) error {
|
||||||
|
s.logger.Info("Running GRPC server", "address", s.cfg.GRPCServerAddress, "network", s.cfg.GRPCServerNetwork, "tls", s.cfg.GRPCServerTLSConfig != nil)
|
||||||
|
|
||||||
|
listener, err := net.Listen(s.cfg.GRPCServerNetwork, s.cfg.GRPCServerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GRPC server: failed to listen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serveErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
s.logger.Info("GRPC server: starting")
|
||||||
|
err := s.server.Serve(listener)
|
||||||
|
if err != nil {
|
||||||
|
backend.Logger.Error("GRPC server: failed to serve", "err", err)
|
||||||
|
serveErr <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-serveErr:
|
||||||
|
backend.Logger.Error("GRPC server: failed to serve", "err", err)
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
s.logger.Warn("GRPC server: shutting down")
|
||||||
|
s.server.Stop()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GPRCServerService) IsDisabled() bool {
|
||||||
|
if s.cfg == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !s.cfg.IsFeatureToggleEnabled(featuremgmt.FlagGrpcServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GPRCServerService) GetServer() *grpc.Server {
|
||||||
|
return s.server
|
||||||
|
}
|
@ -5,9 +5,11 @@ package setting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -463,6 +465,10 @@ type Cfg struct {
|
|||||||
RBACPermissionCache bool
|
RBACPermissionCache bool
|
||||||
// Enable Permission validation during role creation and provisioning
|
// Enable Permission validation during role creation and provisioning
|
||||||
RBACPermissionValidationEnabled bool
|
RBACPermissionValidationEnabled bool
|
||||||
|
// GRPC Server.
|
||||||
|
GRPCServerNetwork string
|
||||||
|
GRPCServerAddress string
|
||||||
|
GRPCServerTLSConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@ -946,6 +952,10 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := readGRPCServerSettings(cfg, iniFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// read dashboard settings
|
// read dashboard settings
|
||||||
dashboards := iniFile.Section("dashboards")
|
dashboards := iniFile.Section("dashboards")
|
||||||
DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
|
DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
|
||||||
@ -1492,6 +1502,68 @@ func readAlertingSettings(iniFile *ini.File) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readGRPCServerSettings(cfg *Cfg, iniFile *ini.File) error {
|
||||||
|
server := iniFile.Section("grpc_server")
|
||||||
|
errPrefix := "grpc_server:"
|
||||||
|
useTLS := server.Key("use_tls").MustBool(false)
|
||||||
|
certFile := server.Key("cert_file").String()
|
||||||
|
keyFile := server.Key("cert_key").String()
|
||||||
|
if useTLS {
|
||||||
|
serverCert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s error loading X509 key pair: %w", errPrefix, err)
|
||||||
|
}
|
||||||
|
cfg.GRPCServerTLSConfig = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{serverCert},
|
||||||
|
ClientAuth: tls.NoClientCert,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.GRPCServerNetwork = valueAsString(server, "network", "tcp")
|
||||||
|
cfg.GRPCServerAddress = valueAsString(server, "address", "")
|
||||||
|
switch cfg.GRPCServerNetwork {
|
||||||
|
case "unix":
|
||||||
|
if cfg.GRPCServerAddress != "" {
|
||||||
|
// Explicitly provided path for unix domain socket.
|
||||||
|
if stat, err := os.Stat(cfg.GRPCServerAddress); os.IsNotExist(err) {
|
||||||
|
// File does not exist - nice, nothing to do.
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("%s error getting stat for a file: %s", errPrefix, cfg.GRPCServerAddress)
|
||||||
|
} else {
|
||||||
|
if stat.Mode()&fs.ModeSocket == 0 {
|
||||||
|
return fmt.Errorf("%s file %s already exists and is not a unix domain socket", errPrefix, cfg.GRPCServerAddress)
|
||||||
|
}
|
||||||
|
// Unix domain socket file, should be safe to remove.
|
||||||
|
err := os.Remove(cfg.GRPCServerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s can't remove unix socket file: %s", errPrefix, cfg.GRPCServerAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use temporary file path for a unix domain socket.
|
||||||
|
tf, err := os.CreateTemp("", "gf_grpc_server_api")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s error creating tmp file: %v", errPrefix, err)
|
||||||
|
}
|
||||||
|
unixPath := tf.Name()
|
||||||
|
if err := tf.Close(); err != nil {
|
||||||
|
return fmt.Errorf("%s error closing tmp file: %v", errPrefix, err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(unixPath); err != nil {
|
||||||
|
return fmt.Errorf("%s error removing tmp file: %v", errPrefix, err)
|
||||||
|
}
|
||||||
|
cfg.GRPCServerAddress = unixPath
|
||||||
|
}
|
||||||
|
case "tcp":
|
||||||
|
if cfg.GRPCServerAddress == "" {
|
||||||
|
cfg.GRPCServerAddress = "127.0.0.1:10000"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s unsupported network %s", errPrefix, cfg.GRPCServerNetwork)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsLegacyAlertingEnabled returns whether the legacy alerting is enabled or not.
|
// IsLegacyAlertingEnabled returns whether the legacy alerting is enabled or not.
|
||||||
// It's safe to be used only after readAlertingSettings() and ReadUnifiedAlertingSettings() are executed.
|
// It's safe to be used only after readAlertingSettings() and ReadUnifiedAlertingSettings() are executed.
|
||||||
func IsLegacyAlertingEnabled() bool {
|
func IsLegacyAlertingEnabled() bool {
|
||||||
|
Loading…
Reference in New Issue
Block a user