mirror of https://github.com/grafana/grafana
Storage: Move grpc helper from entity store to resource store (#89490)
parent
d988f5c3b0
commit
5e95c1bdf8
@ -1,100 +0,0 @@ |
|||||||
package grpc |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"google.golang.org/grpc" |
|
||||||
"google.golang.org/grpc/metadata" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity" |
|
||||||
"github.com/grafana/grafana/pkg/infra/appcontext" |
|
||||||
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors" |
|
||||||
"github.com/grafana/grafana/pkg/services/user" |
|
||||||
) |
|
||||||
|
|
||||||
type Authenticator struct{} |
|
||||||
|
|
||||||
func (f *Authenticator) Authenticate(ctx context.Context) (context.Context, error) { |
|
||||||
md, ok := metadata.FromIncomingContext(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("no metadata found") |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: use id token instead of these fields
|
|
||||||
login := md.Get("grafana-login")[0] |
|
||||||
if login == "" { |
|
||||||
return nil, fmt.Errorf("no login found in context") |
|
||||||
} |
|
||||||
userID, err := strconv.ParseInt(md.Get("grafana-userid")[0], 10, 64) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid user id: %w", err) |
|
||||||
} |
|
||||||
orgID, err := strconv.ParseInt(md.Get("grafana-orgid")[0], 10, 64) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid org id: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: validate id token
|
|
||||||
/* |
|
||||||
idToken := md.Get("grafana-idtoken")[0] |
|
||||||
if idToken == "" { |
|
||||||
return nil, fmt.Errorf("no id token found in context") |
|
||||||
} |
|
||||||
jwtToken, err := jwt.ParseSigned(idToken) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid id token: %w", err) |
|
||||||
} |
|
||||||
claims := jwt.Claims{} |
|
||||||
err = jwtToken.UnsafeClaimsWithoutVerification(&claims) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid id token: %w", err) |
|
||||||
} |
|
||||||
// fmt.Printf("JWT CLAIMS: %+v\n", claims)
|
|
||||||
*/ |
|
||||||
|
|
||||||
return appcontext.WithUser(ctx, &user.SignedInUser{ |
|
||||||
Login: login, |
|
||||||
UserID: userID, |
|
||||||
OrgID: orgID, |
|
||||||
}), nil |
|
||||||
} |
|
||||||
|
|
||||||
var _ interceptors.Authenticator = (*Authenticator)(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, metadata.Pairs( |
|
||||||
"grafana-idtoken", user.GetIDToken(), |
|
||||||
"grafana-userid", user.GetID().ID(), |
|
||||||
"grafana-useruid", user.GetUID().ID(), |
|
||||||
"grafana-orgid", strconv.FormatInt(user.GetOrgID(), 10), |
|
||||||
"grafana-login", user.GetLogin(), |
|
||||||
)), nil |
|
||||||
} |
|
||||||
@ -1,8 +1,20 @@ |
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= |
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= |
||||||
|
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= |
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= |
||||||
|
github.com/grafana/authlib v0.0.0-20240611075137-331cbe4e840f h1:hvRCAv+TgcHu3i/Sd7lFJx84iEtgzDCYuk7OWeXatD0= |
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= |
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= |
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= |
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= |
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= |
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= |
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= |
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= |
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= |
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= |
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= |
||||||
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= |
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= |
||||||
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= |
||||||
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= |
||||||
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= |
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= |
||||||
|
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= |
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= |
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= |
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= |
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
||||||
|
|||||||
@ -0,0 +1,163 @@ |
|||||||
|
package grpc |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"github.com/grafana/authlib/authn" |
||||||
|
"google.golang.org/grpc" |
||||||
|
"google.golang.org/grpc/metadata" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
mdToken = "grafana-idtoken" |
||||||
|
mdLogin = "grafana-login" |
||||||
|
mdUserID = "grafana-user-id" |
||||||
|
mdUserUID = "grafana-user-uid" |
||||||
|
mdOrgName = "grafana-org-name" |
||||||
|
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 verisons have a "-" in the key
|
||||||
|
// TODO, remove after this has been deployed to unified storage
|
||||||
|
if getter(mdUserID) == "" { |
||||||
|
var err error |
||||||
|
user.Namespace = identity.NamespaceUser |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
ns, err := identity.ParseNamespaceID(getter(mdUserID)) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err) |
||||||
|
} |
||||||
|
user.Namespace = ns.Namespace() |
||||||
|
user.UserID, err = ns.ParseInt() |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
ns, err = identity.ParseNamespaceID(getter(mdUserUID)) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err) |
||||||
|
} |
||||||
|
user.UserUID = ns.ID() |
||||||
|
|
||||||
|
user.OrgName = getter(mdOrgName) |
||||||
|
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 { |
||||||
|
return metadata.Pairs( |
||||||
|
// This should be everything needed to recreate the user
|
||||||
|
mdToken, user.GetIDToken(), |
||||||
|
|
||||||
|
// Or we can create it directly
|
||||||
|
mdUserID, user.GetID().String(), |
||||||
|
mdUserUID, user.GetUID().String(), |
||||||
|
mdOrgName, user.GetOrgName(), |
||||||
|
mdOrgID, strconv.FormatInt(user.GetOrgID(), 10), |
||||||
|
mdOrgRole, string(user.GetOrgRole()), |
||||||
|
mdLogin, user.GetLogin(), |
||||||
|
|
||||||
|
// TODO, Remove after this is deployed to unified storage
|
||||||
|
"grafana-userid", user.GetID().ID(), |
||||||
|
"grafana-useruid", user.GetUID().ID(), |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
package grpc |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity" |
||||||
|
) |
||||||
|
|
||||||
|
func TestBasicEncodeDecode(t *testing.T) { |
||||||
|
before := &identity.StaticRequester{ |
||||||
|
UserID: 123, |
||||||
|
UserUID: "abc", |
||||||
|
Login: "test", |
||||||
|
Namespace: identity.NamespaceUser, |
||||||
|
OrgID: 456, |
||||||
|
OrgName: "org", |
||||||
|
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.GetLogin(), after.GetLogin()) |
||||||
|
require.Equal(t, before.GetOrgID(), after.GetOrgID()) |
||||||
|
require.Equal(t, before.GetOrgName(), after.GetOrgName()) |
||||||
|
require.Equal(t, before.GetOrgRole(), after.GetOrgRole()) |
||||||
|
} |
||||||
Loading…
Reference in new issue