AuthN: Use tokens for unified storage server authentication (#95086)

* Extract server code

---------

Co-authored-by: Claudiu Dragalina-Paraipan <drclau@users.noreply.github.com>
pull/95254/head
Gabriel MABILLE 8 months ago committed by GitHub
parent 9ab064bfc5
commit b68b69c2b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 45
      pkg/services/authn/grpcutils/config.go
  2. 118
      pkg/services/authn/grpcutils/grpc_authenticator.go
  3. 8
      pkg/storage/unified/README.md
  4. 8
      pkg/storage/unified/sql/service.go
  5. 12
      pkg/tests/testinfra/testinfra.go

@ -0,0 +1,45 @@
package grpcutils
import (
"fmt"
"github.com/grafana/grafana/pkg/setting"
)
type Mode string
func (s Mode) IsValid() bool {
switch s {
case ModeOnPrem, ModeCloud:
return true
}
return false
}
const (
ModeOnPrem Mode = "on-prem"
ModeCloud Mode = "cloud"
)
type GrpcServerConfig struct {
SigningKeysURL string
AllowedAudiences []string
Mode Mode
LegacyFallback bool
}
func ReadGrpcServerConfig(cfg *setting.Cfg) (*GrpcServerConfig, error) {
section := cfg.SectionWithEnvOverrides("grpc_server_authentication")
mode := Mode(section.Key("mode").MustString(string(ModeOnPrem)))
if !mode.IsValid() {
return nil, fmt.Errorf("grpc_server_authentication: invalid mode %q", mode)
}
return &GrpcServerConfig{
SigningKeysURL: section.Key("signing_keys_url").MustString(""),
AllowedAudiences: section.Key("allowed_audiences").Strings(","),
Mode: mode,
LegacyFallback: section.Key("legacy_fallback").MustBool(true),
}, nil
}

@ -1,9 +1,21 @@
package grpcutils
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"sync"
authnlib "github.com/grafana/authlib/authn"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
"github.com/grafana/grafana/pkg/setting"
)
var once sync.Once
func NewInProcGrpcAuthenticator() *authnlib.GrpcAuthenticator {
// In proc grpc ID token signature verification can be skipped
return authnlib.NewUnsafeGrpcAuthenticator(
@ -12,3 +24,109 @@ func NewInProcGrpcAuthenticator() *authnlib.GrpcAuthenticator {
authnlib.WithIDTokenAuthOption(true),
)
}
func NewGrpcAuthenticator(cfg *setting.Cfg) (*authnlib.GrpcAuthenticator, error) {
authCfg, err := ReadGrpcServerConfig(cfg)
if err != nil {
return nil, err
}
grpcAuthCfg := authnlib.GrpcAuthenticatorConfig{
KeyRetrieverConfig: authnlib.KeyRetrieverConfig{
SigningKeysURL: authCfg.SigningKeysURL,
},
VerifierConfig: authnlib.VerifierConfig{
AllowedAudiences: authCfg.AllowedAudiences,
},
}
client := http.DefaultClient
if cfg.Env == setting.Dev {
// allow insecure connections in development mode to facilitate testing
client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
}
keyRetriever := authnlib.NewKeyRetriever(grpcAuthCfg.KeyRetrieverConfig, authnlib.WithHTTPClientKeyRetrieverOpt(client))
grpcOpts := []authnlib.GrpcAuthenticatorOption{
authnlib.WithIDTokenAuthOption(true),
authnlib.WithKeyRetrieverOption(keyRetriever),
}
if authCfg.Mode == ModeOnPrem {
grpcOpts = append(grpcOpts,
// Access token are not yet available on-prem
authnlib.WithDisableAccessTokenAuthOption(),
)
}
return authnlib.NewGrpcAuthenticator(
&grpcAuthCfg,
grpcOpts...,
)
}
type AuthenticatorWithFallback struct {
authenticator *authnlib.GrpcAuthenticator
fallback interceptors.Authenticator
metrics *metrics
}
func NewGrpcAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer, fallback interceptors.Authenticator) (interceptors.Authenticator, error) {
authCfg, err := ReadGrpcServerConfig(cfg)
if err != nil {
return nil, err
}
authenticator, err := NewGrpcAuthenticator(cfg)
if err != nil {
return nil, err
}
if !authCfg.LegacyFallback {
return authenticator, nil
}
return &AuthenticatorWithFallback{
authenticator: authenticator,
fallback: fallback,
metrics: newMetrics(reg),
}, nil
}
func (f *AuthenticatorWithFallback) Authenticate(ctx context.Context) (context.Context, error) {
// Try to authenticate with the new authenticator first
newCtx, err := f.authenticator.Authenticate(ctx)
if err != nil {
// In case of error, fallback to the legacy authenticator
newCtx, err = f.fallback.Authenticate(ctx)
f.metrics.fallbackCounter.WithLabelValues(fmt.Sprintf("%t", err == nil)).Inc()
}
return newCtx, err
}
const (
metricsNamespace = "grafana"
metricsSubSystem = "grpc_authenticator"
)
type metrics struct {
fallbackCounter *prometheus.CounterVec
}
func newMetrics(reg prometheus.Registerer) *metrics {
m := &metrics{
fallbackCounter: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
Name: "fallback_total",
Help: "Number of times the fallback authenticator was used",
}, []string{"result"}),
}
if reg != nil {
once.Do(func() {
reg.MustRegister(m.fallbackCounter)
})
}
return m
}

@ -240,6 +240,14 @@ Make sure you have the gRPC address in the `[grafana-apiserver]` section of your
address = localhost:10000
```
You also need the `[grpc_server_authentication]` section to authenticate incoming requests:
```ini
[grpc_server_authentication]
; http url to Grafana's signing keys to validate incoming id tokens
signing_keys_url = http://localhost:3000/api/signing-keys/keys
mode = "on-prem"
```
This currently only works with a separate database configuration (see previous section).
Start the storage-server with:

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/modules"
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
@ -67,7 +68,12 @@ func ProvideUnifiedStorageGrpcService(
return nil, err
}
authn := &grpc.Authenticator{}
// FIXME: This is a temporary solution while we are migrating to the new authn interceptor
// grpcutils.NewGrpcAuthenticator should be used instead.
authn, err := grpcutils.NewGrpcAuthenticatorWithFallback(cfg, prometheus.DefaultRegisterer, &grpc.Authenticator{})
if err != nil {
return nil, err
}
s := &service{
cfg: cfg,

@ -53,6 +53,11 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes
serverOpts := server.Options{Listener: listener, HomePath: grafDir}
apiServerOpts := api.ServerOptions{Listener: listener}
// Replace the placeholder in the `signing_keys_url` with the actual address
grpcServerAuthSection := cfg.SectionWithEnvOverrides("grpc_server_authentication")
signingKeysUrl := grpcServerAuthSection.Key("signing_keys_url")
signingKeysUrl.SetValue(strings.Replace(signingKeysUrl.String(), "<placeholder>", listener.Addr().String(), 1))
// Potentially allocate a real gRPC port for unified storage
runstore := false
unistore, _ := cfg.Raw.GetSection("grafana-apiserver")
@ -291,6 +296,13 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
_, err = analyticsSect.NewKey("intercom_secret", "intercom_secret_at_config")
require.NoError(t, err)
grpcServerAuth, err := cfg.NewSection("grpc_server_authentication")
require.NoError(t, err)
_, err = grpcServerAuth.NewKey("signing_keys_url", "http://<placeholder>/api/signing-keys/keys")
require.NoError(t, err)
_, err = grpcServerAuth.NewKey("allowed_audiences", "org:1")
require.NoError(t, err)
getOrCreateSection := func(name string) (*ini.Section, error) {
section, err := cfg.GetSection(name)
if err != nil {

Loading…
Cancel
Save