diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 6d5f1bb4687..18e38324c26 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -18,6 +18,7 @@ const ( ClientAPIKey = "auth.client.api-key" // #nosec G101 ClientAnonymous = "auth.client.anonymous" ClientBasic = "auth.client.basic" + ClientRender = "auth.client.render" ) type ClientParams struct { diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index bf58c809a2e..7269d120120 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/loginattempt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "go.opentelemetry.io/otel/attribute" @@ -27,7 +28,7 @@ var _ authn.Service = new(Service) func ProvideService( cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service, accessControlService accesscontrol.Service, apikeyService apikey.Service, userService user.Service, loginAttempts loginattempt.Service, quotaService quota.Service, - authInfoService login.AuthInfoService, + authInfoService login.AuthInfoService, renderService rendering.Service, ) *Service { s := &Service{ log: log.New("authn.service"), @@ -37,6 +38,7 @@ func ProvideService( postAuthHooks: []authn.PostAuthHookFn{}, } + s.clients[authn.ClientRender] = clients.ProvideRender(userService, renderService) s.clients[authn.ClientAPIKey] = clients.ProvideAPIKey(apikeyService, userService) if s.cfg.AnonymousEnabled { diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go new file mode 100644 index 00000000000..4207d4c7cdd --- /dev/null +++ b/pkg/services/authn/clients/render.go @@ -0,0 +1,71 @@ +package clients + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util/errutil" +) + +var ( + ErrInvalidRenderKey = errutil.NewBase(errutil.StatusUnauthorized, "render-auth.invalid-key", errutil.WithPublicMessage("Invalid Render Key")) +) + +const ( + renderCookieName = "renderKey" +) + +var _ authn.Client = new(Render) + +func ProvideRender(userService user.Service, renderService rendering.Service) *Render { + return &Render{userService, renderService} +} + +type Render struct { + userService user.Service + renderService rendering.Service +} + +func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + key := getRenderKey(r) + renderUsr, ok := c.renderService.GetRenderUser(ctx, key) + if !ok { + return nil, ErrInvalidRenderKey.Errorf("found no render user for key: %s", key) + } + + if renderUsr.UserID <= 0 { + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceUser, 0), + OrgID: renderUsr.OrgID, + OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, + }, nil + } + + usr, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{UserID: renderUsr.UserID, OrgID: renderUsr.OrgID}) + if err != nil { + return nil, err + } + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr), nil +} + +func (c *Render) ClientParams() *authn.ClientParams { + return &authn.ClientParams{} +} + +func (c *Render) Test(ctx context.Context, r *authn.Request) bool { + if r.HTTPRequest == nil { + return false + } + return getRenderKey(r) != "" +} + +func getRenderKey(r *authn.Request) string { + cookie, err := r.HTTPRequest.Cookie(renderCookieName) + if err != nil { + return "" + } + return cookie.Value +} diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go new file mode 100644 index 00000000000..63b283dadf0 --- /dev/null +++ b/pkg/services/authn/clients/render_test.go @@ -0,0 +1,139 @@ +package clients + +import ( + "context" + "net/http" + "testing" + + "github.com/golang/mock/gomock" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/stretchr/testify/assert" +) + +func TestRender_Authenticate(t *testing.T) { + type TestCase struct { + desc string + renderKey string + req *authn.Request + expectedErr error + expectedUsr *user.SignedInUser + expectedIdentity *authn.Identity + expectedRenderUsr *rendering.RenderUser + } + + tests := []TestCase{ + { + desc: "expect valid render key to return render user identity", + renderKey: "123", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Cookie": {"renderKey=123"}}, + }, + }, + expectedIdentity: &authn.Identity{ + ID: "user:0", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + }, + expectedRenderUsr: &rendering.RenderUser{ + OrgID: 1, + UserID: 0, + OrgRole: "Viewer", + }, + }, + { + desc: "expect valid render key connected to user to return identity", + renderKey: "123", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Cookie": {"renderKey=123"}}, + }, + }, + expectedIdentity: &authn.Identity{ + ID: "user:1", + OrgID: 1, + OrgName: "test", + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + IsGrafanaAdmin: boolPtr(false), + }, + expectedRenderUsr: &rendering.RenderUser{ + OrgID: 1, + UserID: 1, + }, + expectedUsr: &user.SignedInUser{ + UserID: 1, + OrgID: 1, + OrgName: "test", + OrgRole: "Admin", + }, + }, + { + desc: "expect error when render key is invalid", + renderKey: "123", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Cookie": {"renderKey=123"}}, + }, + }, + expectedErr: ErrInvalidRenderKey, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + renderService := rendering.NewMockService(ctrl) + renderService.EXPECT().GetRenderUser(gomock.Any(), tt.renderKey).Return(tt.expectedRenderUsr, tt.expectedRenderUsr != nil) + + c := ProvideRender(&usertest.FakeUserService{ExpectedSignedInUser: tt.expectedUsr}, renderService) + identity, err := c.Authenticate(context.Background(), tt.req) + if tt.expectedErr != nil { + assert.ErrorIs(t, tt.expectedErr, err) + assert.Nil(t, identity) + } else { + assert.NoError(t, err) + assert.EqualValues(t, *tt.expectedIdentity, *identity) + } + }) + } +} + +func TestRender_Test(t *testing.T) { + type TestCase struct { + desc string + req *authn.Request + expected bool + } + + tests := []TestCase{ + { + desc: "should success when request has render cookie available", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Cookie": {"renderKey=123"}}, + }}, + expected: true, + }, + { + desc: "should fail if no http request is passed", + req: &authn.Request{}, + }, + { + desc: "should fail if no renderKey cookie is present in request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Cookie": {"notRenderKey=123"}}, + }}, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvideRender(&usertest.FakeUserService{}, &rendering.MockService{}) + assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) + }) + } +} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 7daa0605eed..397b7bec368 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -599,6 +599,24 @@ func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) w } func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext) bool { + if h.features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientRender, &authn.Request{HTTPRequest: reqContext.Req}) + if !ok { + return false + } + + if err != nil { + writeErr(reqContext, err) + return true + } + + reqContext.IsSignedIn = true + reqContext.IsRenderCall = true + reqContext.LastSeenAt = time.Now() + reqContext.SignedInUser = identity.SignedInUser() + return true + } + key := reqContext.GetCookie("renderKey") if key == "" { return false