diff --git a/pkg/services/apiserver/aggregator/README.md b/pkg/services/apiserver/aggregator/README.md index bf8a6a5a21b..3537e723f43 100644 --- a/pkg/services/apiserver/aggregator/README.md +++ b/pkg/services/apiserver/aggregator/README.md @@ -92,7 +92,11 @@ configuration overwrites on startup. go run ./pkg/cmd/grafana apiserver \ --runtime-config=example.grafana.app/v0alpha1=true \ --secure-port 7443 \ - --client-ca-file=$PWD/data/grafana-aggregator/ca.crt + --requestheader-client-ca-file=$PWD/data/grafana-aggregator/ca.crt \ + --requestheader-extra-headers-prefix=X-Remote-Extra- \ + --requestheader-group-headers=X-Remote-Group \ + --requestheader-username-headers=X-Remote-User \ + -v 10 ``` 6. After 10 seconds, check `APIService` again. It should report as available. ```shell diff --git a/pkg/services/apiserver/auth/authenticator/provider.go b/pkg/services/apiserver/auth/authenticator/provider.go new file mode 100644 index 00000000000..389c808d528 --- /dev/null +++ b/pkg/services/apiserver/auth/authenticator/provider.go @@ -0,0 +1,11 @@ +package authenticator + +import ( + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/request/union" +) + +func NewAuthenticator(authRequestHandlers ...authenticator.Request) authenticator.Request { + handlers := append([]authenticator.Request{authenticator.RequestFunc(signedInUserAuthenticator)}, authRequestHandlers...) + return union.New(handlers...) +} diff --git a/pkg/services/apiserver/auth/authenticator/signedinuser.go b/pkg/services/apiserver/auth/authenticator/signedinuser.go new file mode 100644 index 00000000000..b407976dcff --- /dev/null +++ b/pkg/services/apiserver/auth/authenticator/signedinuser.go @@ -0,0 +1,42 @@ +package authenticator + +import ( + "net/http" + "strconv" + + "k8s.io/apiserver/pkg/authentication/authenticator" + k8suser "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/klog/v2" + + "github.com/grafana/grafana/pkg/infra/appcontext" +) + +var _ authenticator.RequestFunc = signedInUserAuthenticator + +func signedInUserAuthenticator(req *http.Request) (*authenticator.Response, bool, error) { + ctx := req.Context() + signedInUser, err := appcontext.User(ctx) + if err != nil { + klog.V(5).Info("failed to get signed in user", "err", err) + return nil, false, nil + } + + userInfo := &k8suser.DefaultInfo{ + Name: signedInUser.Login, + UID: signedInUser.UserUID, + Groups: []string{}, + Extra: map[string][]string{}, + } + + for _, v := range signedInUser.Teams { + userInfo.Groups = append(userInfo.Groups, strconv.FormatInt(v, 10)) + } + + if signedInUser.IDToken != "" { + userInfo.Extra["ID-Token"] = []string{signedInUser.IDToken} + } + + return &authenticator.Response{ + User: userInfo, + }, true, nil +} diff --git a/pkg/services/apiserver/auth/authenticator/signedinuser_test.go b/pkg/services/apiserver/auth/authenticator/signedinuser_test.go new file mode 100644 index 00000000000..79cc041a213 --- /dev/null +++ b/pkg/services/apiserver/auth/authenticator/signedinuser_test.go @@ -0,0 +1,88 @@ +package authenticator + +import ( + "context" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/request/union" +) + +func TestSignedInUser(t *testing.T) { + t.Run("should call next authenticator if SignedInUser is not set", func(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil) + require.NoError(t, err) + mockAuthenticator := &mockAuthenticator{} + all := union.New(authenticator.RequestFunc(signedInUserAuthenticator), mockAuthenticator) + res, ok, err := all.AuthenticateRequest(req) + require.NoError(t, err) + require.False(t, ok) + require.Nil(t, res) + require.True(t, mockAuthenticator.called) + }) + + t.Run("should set user and group", func(t *testing.T) { + u := &user.SignedInUser{ + Login: "admin", + UserID: 1, + UserUID: uuid.New().String(), + Teams: []int64{1, 2}, + } + ctx := appcontext.WithUser(context.Background(), u) + req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil) + require.NoError(t, err) + req = req.WithContext(ctx) + mockAuthenticator := &mockAuthenticator{} + all := union.New(authenticator.RequestFunc(signedInUserAuthenticator), mockAuthenticator) + res, ok, err := all.AuthenticateRequest(req) + require.NoError(t, err) + require.True(t, ok) + require.False(t, mockAuthenticator.called) + + require.Equal(t, u.Login, res.User.GetName()) + require.Equal(t, u.UserUID, res.User.GetUID()) + require.Equal(t, []string{"1", "2"}, res.User.GetGroups()) + require.Empty(t, res.User.GetExtra()["ID-Token"]) + }) + + t.Run("should set ID token when available", func(t *testing.T) { + u := &user.SignedInUser{ + Login: "admin", + UserID: 1, + UserUID: uuid.New().String(), + Teams: []int64{1, 2}, + IDToken: "test-id-token", + } + ctx := appcontext.WithUser(context.Background(), u) + req, err := http.NewRequest("GET", "http://localhost:3000/apis", nil) + require.NoError(t, err) + req = req.WithContext(ctx) + mockAuthenticator := &mockAuthenticator{} + all := union.New(authenticator.RequestFunc(signedInUserAuthenticator), mockAuthenticator) + res, ok, err := all.AuthenticateRequest(req) + require.NoError(t, err) + require.True(t, ok) + + require.False(t, mockAuthenticator.called) + require.Equal(t, u.Login, res.User.GetName()) + require.Equal(t, u.UserUID, res.User.GetUID()) + require.Equal(t, []string{"1", "2"}, res.User.GetGroups()) + require.Equal(t, "test-id-token", res.User.GetExtra()["ID-Token"][0]) + }) +} + +var _ authenticator.Request = (*mockAuthenticator)(nil) + +type mockAuthenticator struct { + called bool +} + +func (a *mockAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + a.called = true + return nil, false, nil +} diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 8233c4bd51c..aaa5f4ad376 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/modules" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/apiserver/aggregator" + "github.com/grafana/grafana/pkg/services/apiserver/auth/authenticator" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/builder" grafanaresponsewriter "github.com/grafana/grafana/pkg/services/apiserver/endpoints/responsewriter" @@ -220,6 +221,7 @@ func (s *service) start(ctx context.Context) error { return err } serverConfig.Authorization.Authorizer = s.authorizer + serverConfig.Authentication.Authenticator = authenticator.NewAuthenticator(serverConfig.Authentication.Authenticator) serverConfig.TracerProvider = s.tracing.GetTracerProvider() // setup loopback transport for the aggregator server