added server side gRPC authn fallback-to-legacy mechanism
- brought back the old gRPC authenticator - added `grpc_server_authentication.legacy_fallback` config option - introduced `AuthenticatorWithFallback` - added telemetry to track fallbacks
This commit is contained in:
@@ -48,6 +48,7 @@ type GrpcServerConfig struct {
|
|||||||
SigningKeysURL string
|
SigningKeysURL string
|
||||||
AllowedAudiences []string
|
AllowedAudiences []string
|
||||||
Mode Mode
|
Mode Mode
|
||||||
|
LegacyFallback bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadGrpcServerConfig(cfg *setting.Cfg) (*GrpcServerConfig, error) {
|
func ReadGrpcServerConfig(cfg *setting.Cfg) (*GrpcServerConfig, error) {
|
||||||
@@ -62,5 +63,6 @@ func ReadGrpcServerConfig(cfg *setting.Cfg) (*GrpcServerConfig, error) {
|
|||||||
SigningKeysURL: section.Key("signing_keys_url").MustString(""),
|
SigningKeysURL: section.Key("signing_keys_url").MustString(""),
|
||||||
AllowedAudiences: section.Key("allowed_audiences").Strings(","),
|
AllowedAudiences: section.Key("allowed_audiences").Strings(","),
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
|
LegacyFallback: section.Key("legacy_fallback").MustBool(true),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package grpcutils
|
package grpcutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
authnlib "github.com/grafana/authlib/authn"
|
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"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGrpcAuthenticator(cfg *setting.Cfg) (*authnlib.GrpcAuthenticator, error) {
|
func NewGrpcAuthenticator(cfg *setting.Cfg) (*authnlib.GrpcAuthenticator, error) {
|
||||||
@@ -55,3 +60,72 @@ func NewInProcGrpcAuthenticator() *authnlib.GrpcAuthenticator {
|
|||||||
authnlib.WithIDTokenAuthOption(true),
|
authnlib.WithIDTokenAuthOption(true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthenticatorWithFallback struct {
|
||||||
|
authenticator *authnlib.GrpcAuthenticator
|
||||||
|
legacyAuthenticator *grpc.Authenticator
|
||||||
|
fallbackEnabled bool
|
||||||
|
metrics *metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGrpcAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer) (interceptors.Authenticator, error) {
|
||||||
|
authCfg, err := ReadGrpcServerConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticator, err := NewGrpcAuthenticator(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyAuthenticator := &grpc.Authenticator{}
|
||||||
|
|
||||||
|
return &AuthenticatorWithFallback{
|
||||||
|
authenticator: authenticator,
|
||||||
|
legacyAuthenticator: legacyAuthenticator,
|
||||||
|
fallbackEnabled: authCfg.LegacyFallback,
|
||||||
|
metrics: newMetrics(reg),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *AuthenticatorWithFallback) Authenticate(ctx context.Context) (context.Context, error) {
|
||||||
|
// Try to authenticate with the new authenticator first
|
||||||
|
ctx, err := f.authenticator.Authenticate(ctx)
|
||||||
|
if err == nil {
|
||||||
|
// If successful, return the context
|
||||||
|
return ctx, nil
|
||||||
|
} else if f.fallbackEnabled {
|
||||||
|
// If the new authenticator failed and the fallback is enabled, try the legacy authenticator
|
||||||
|
ctx, err = f.legacyAuthenticator.Authenticate(ctx)
|
||||||
|
f.metrics.fallbackCounter.WithLabelValues(fmt.Sprintf("%t", err == nil)).Inc()
|
||||||
|
}
|
||||||
|
return ctx, 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 {
|
||||||
|
reg.MustRegister(m.fallbackCounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/authn"
|
||||||
|
authClaims "github.com/grafana/authlib/claims"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mdToken = "grafana-idtoken"
|
||||||
|
mdLogin = "grafana-login"
|
||||||
|
mdUserID = "grafana-user-id"
|
||||||
|
mdUserUID = "grafana-user-uid"
|
||||||
|
mdOrgID = "grafana-org-id"
|
||||||
|
mdOrgRole = "grafana-org-role"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is in a package we can no import
|
||||||
|
// var _ interceptors.Authenticator = (*Authenticator)(nil)
|
||||||
|
|
||||||
|
type Authenticator struct {
|
||||||
|
IDTokenVerifier authn.Verifier[authn.IDTokenClaims]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Authenticator) Authenticate(ctx context.Context) (context.Context, error) {
|
||||||
|
r, err := identity.GetRequester(ctx)
|
||||||
|
if err == nil && r != nil {
|
||||||
|
return ctx, nil // noop, requester exists
|
||||||
|
}
|
||||||
|
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no metadata found")
|
||||||
|
}
|
||||||
|
user, err := f.decodeMetadata(ctx, md)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return identity.WithRequester(ctx, user), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Authenticator) decodeMetadata(ctx context.Context, meta metadata.MD) (identity.Requester, error) {
|
||||||
|
// Avoid NPE/panic with getting keys
|
||||||
|
getter := func(key string) string {
|
||||||
|
v := meta.Get(key)
|
||||||
|
if len(v) > 0 {
|
||||||
|
return v[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try the token
|
||||||
|
token := getter(mdToken)
|
||||||
|
if token != "" && f.IDTokenVerifier != nil {
|
||||||
|
claims, err := f.IDTokenVerifier.Verify(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Printf("TODO, convert CLAIMS to an identity %+v\n", claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &identity.StaticRequester{}
|
||||||
|
user.Login = getter(mdLogin)
|
||||||
|
if user.Login == "" {
|
||||||
|
return nil, fmt.Errorf("no login found in grpc metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The namespaced versions have a "-" in the key
|
||||||
|
// TODO, remove after this has been deployed to unified storage
|
||||||
|
if getter(mdUserID) == "" {
|
||||||
|
var err error
|
||||||
|
user.Type = authClaims.TypeUser
|
||||||
|
user.UserID, err = strconv.ParseInt(getter("grafana-userid"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err)
|
||||||
|
}
|
||||||
|
user.OrgID, err = strconv.ParseInt(getter("grafana-orgid"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid org id: %w", err)
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
typ, id, err := identity.ParseTypeAndID(getter(mdUserID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err)
|
||||||
|
}
|
||||||
|
user.Type = typ
|
||||||
|
user.UserID, err = strconv.ParseInt(id, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, id, err = identity.ParseTypeAndID(getter(mdUserUID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err)
|
||||||
|
}
|
||||||
|
user.UserUID = id
|
||||||
|
|
||||||
|
user.OrgID, err = strconv.ParseInt(getter(mdOrgID), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid org id: %w", err)
|
||||||
|
}
|
||||||
|
user.OrgRole = identity.RoleType(getter(mdOrgRole))
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||||
|
ctx, err := wrapContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return invoker(ctx, method, req, reply, cc, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ grpc.UnaryClientInterceptor = UnaryClientInterceptor
|
||||||
|
|
||||||
|
func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||||
|
ctx, err := wrapContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return streamer(ctx, desc, cc, method, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ grpc.StreamClientInterceptor = StreamClientInterceptor
|
||||||
|
|
||||||
|
func wrapContext(ctx context.Context) (context.Context, error) {
|
||||||
|
user, err := identity.GetRequester(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set grpc metadata into the context to pass to the grpc server
|
||||||
|
return metadata.NewOutgoingContext(ctx, encodeIdentityInMetadata(user)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeIdentityInMetadata(user identity.Requester) metadata.MD {
|
||||||
|
id, _ := user.GetInternalID()
|
||||||
|
|
||||||
|
return metadata.Pairs(
|
||||||
|
// This should be everything needed to recreate the user
|
||||||
|
mdToken, user.GetIDToken(),
|
||||||
|
|
||||||
|
// Or we can create it directly
|
||||||
|
mdUserID, user.GetID(),
|
||||||
|
mdUserUID, user.GetUID(),
|
||||||
|
mdOrgID, strconv.FormatInt(user.GetOrgID(), 10),
|
||||||
|
mdOrgRole, string(user.GetOrgRole()),
|
||||||
|
mdLogin, user.GetLogin(),
|
||||||
|
|
||||||
|
// TODO, Remove after this is deployed to unified storage
|
||||||
|
"grafana-userid", strconv.FormatInt(id, 10),
|
||||||
|
"grafana-useruid", user.GetRawIdentifier(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/claims"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasicEncodeDecode(t *testing.T) {
|
||||||
|
before := &identity.StaticRequester{
|
||||||
|
UserID: 123,
|
||||||
|
UserUID: "abc",
|
||||||
|
Login: "test",
|
||||||
|
Type: claims.TypeUser,
|
||||||
|
OrgID: 456,
|
||||||
|
OrgRole: identity.RoleAdmin,
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := &Authenticator{}
|
||||||
|
|
||||||
|
md := encodeIdentityInMetadata(before)
|
||||||
|
after, err := auth.decodeMetadata(context.Background(), md)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, before.GetID(), after.GetID())
|
||||||
|
require.Equal(t, before.GetUID(), after.GetUID())
|
||||||
|
require.Equal(t, before.GetIdentityType(), after.GetIdentityType())
|
||||||
|
require.Equal(t, before.GetLogin(), after.GetLogin())
|
||||||
|
require.Equal(t, before.GetOrgID(), after.GetOrgID())
|
||||||
|
require.Equal(t, before.GetOrgName(), after.GetOrgName())
|
||||||
|
require.Equal(t, before.GetOrgRole(), after.GetOrgRole())
|
||||||
|
}
|
||||||
@@ -65,7 +65,7 @@ func ProvideUnifiedStorageGrpcService(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
authn, err := grpcutils.NewGrpcAuthenticator(cfg)
|
authn, err := grpcutils.NewGrpcAuthenticatorWithFallback(cfg, prometheus.DefaultRegisterer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user