Auth: id response header (#79757)

* Add utility function to check if namespace is any of

* Refactor code to use identity interface
This commit is contained in:
Karl Persson
2023-12-21 14:06:28 +01:00
committed by GitHub
parent 38f176edfb
commit 05d1ce4026
4 changed files with 99 additions and 32 deletions

View File

@@ -67,12 +67,22 @@ type Requester interface {
GetIDToken() string GetIDToken() string
} }
// IsNamespace returns true if namespace matches any expected namespace
func IsNamespace(namespace string, expected ...string) bool {
for _, e := range expected {
if namespace == e {
return true
}
}
return false
}
// IntIdentifier converts a string identifier to an int64. // IntIdentifier converts a string identifier to an int64.
// Applicable for users, service accounts, api keys and renderer service. // Applicable for users, service accounts, api keys and renderer service.
// Errors if the identifier is not initialized or if namespace is not recognized. // Errors if the identifier is not initialized or if namespace is not recognized.
func IntIdentifier(namespace, identifier string) (int64, error) { func IntIdentifier(namespace, identifier string) (int64, error) {
switch namespace { if IsNamespace(namespace, NamespaceUser, NamespaceAPIKey, NamespaceServiceAccount, NamespaceRenderService) {
case NamespaceUser, NamespaceAPIKey, NamespaceServiceAccount, NamespaceRenderService:
id, err := strconv.ParseInt(identifier, 10, 64) id, err := strconv.ParseInt(identifier, 10, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf("unrecognized format for valid namespace %s: %w", namespace, err) return 0, fmt.Errorf("unrecognized format for valid namespace %s: %w", namespace, err)
@@ -98,8 +108,7 @@ func UserIdentifier(namespace, identifier string) (int64, error) {
return 0, nil return 0, nil
} }
switch namespace { if IsNamespace(namespace, NamespaceUser, NamespaceServiceAccount) {
case NamespaceUser, NamespaceServiceAccount:
return userID, nil return userID, nil
} }

View File

@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@@ -15,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
@@ -138,38 +138,26 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
)) ))
if h.Cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil { if h.Cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil {
namespace, id := getNamespaceAndID(reqContext.SignedInUser) reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(reqContext.SignedInUser))
reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(namespace, id))
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
// TODO(kalleep): Refactor to user identity.Requester interface and methods after we have backported this func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) web.BeforeFunc {
func getNamespaceAndID(user *user.SignedInUser) (string, string) {
var namespace, id string
if user.UserID > 0 && user.IsServiceAccount {
id = strconv.Itoa(int(user.UserID))
namespace = "service-account"
} else if user.UserID > 0 {
id = strconv.Itoa(int(user.UserID))
namespace = "user"
} else if user.ApiKeyID > 0 {
id = strconv.Itoa(int(user.ApiKeyID))
namespace = "api-key"
}
return namespace, id
}
func (h *ContextHandler) addIDHeaderEndOfRequestFunc(namespace, id string) web.BeforeFunc {
return func(w web.ResponseWriter) { return func(w web.ResponseWriter) {
if w.Written() { if w.Written() {
return return
} }
if namespace == "" || id == "" { namespace, id := ident.GetNamespacedID()
if !identity.IsNamespace(
namespace,
identity.NamespaceUser,
identity.NamespaceServiceAccount,
identity.NamespaceAPIKey,
) || id == "0" {
return return
} }

View File

@@ -2,6 +2,7 @@ package contexthandler_test
import ( import (
"errors" "errors"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -38,8 +39,9 @@ func TestContextHandler(t *testing.T) {
require.Error(t, c.LookupTokenErr) require.Error(t, c.LookupTokenErr)
}) })
_, err := server.Send(server.NewGetRequest("/api/handler")) res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, res.Body.Close())
}) })
t.Run("should set identity on successful authentication", func(t *testing.T) { t.Run("should set identity on successful authentication", func(t *testing.T) {
@@ -59,8 +61,9 @@ func TestContextHandler(t *testing.T) {
require.NoError(t, c.LookupTokenErr) require.NoError(t, c.LookupTokenErr)
}) })
_, err := server.Send(server.NewGetRequest("/api/handler")) res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, res.Body.Close())
}) })
t.Run("should not set IsSignedIn on anonymous identity", func(t *testing.T) { t.Run("should not set IsSignedIn on anonymous identity", func(t *testing.T) {
@@ -80,8 +83,9 @@ func TestContextHandler(t *testing.T) {
require.NoError(t, c.LookupTokenErr) require.NoError(t, c.LookupTokenErr)
}) })
_, err := server.Send(server.NewGetRequest("/api/handler")) res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, res.Body.Close())
}) })
t.Run("should set IsRenderCall when authenticated by render client", func(t *testing.T) { t.Run("should set IsRenderCall when authenticated by render client", func(t *testing.T) {
@@ -102,8 +106,9 @@ func TestContextHandler(t *testing.T) {
require.NoError(t, c.LookupTokenErr) require.NoError(t, c.LookupTokenErr)
}) })
_, err := server.Send(server.NewGetRequest("/api/handler")) res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, res.Body.Close())
}) })
t.Run("should delete session cookie on invalid session", func(t *testing.T) { t.Run("should delete session cookie on invalid session", func(t *testing.T) {
@@ -175,7 +180,72 @@ func TestContextHandler(t *testing.T) {
assert.Contains(t, list.Items, "Authorization") assert.Contains(t, list.Items, "Authorization")
}) })
_, err := server.Send(server.NewGetRequest("/api/handler")) res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, res.Body.Close())
})
t.Run("id response headers", func(t *testing.T) {
run := func(cfg *setting.Cfg, id string) *http.Response {
handler := contexthandler.ProvideService(
cfg,
tracing.InitializeTracerForTest(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: id}},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {})
res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
return res
}
t.Run("should add id header for user", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.IDResponseHeaderEnabled = true
cfg.IDResponseHeaderPrefix = "X-Grafana"
cfg.IDResponseHeaderNamespaces = map[string]struct{}{"user": {}}
res := run(cfg, "user:1")
require.Equal(t, "user:1", res.Header.Get("X-Grafana-Identity-Id"))
require.NoError(t, res.Body.Close())
})
t.Run("should not add id header for user when id is 0", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.IDResponseHeaderEnabled = true
cfg.IDResponseHeaderPrefix = "X-Grafana"
cfg.IDResponseHeaderNamespaces = map[string]struct{}{"user": {}}
res := run(cfg, "user:0")
require.Empty(t, res.Header.Get("X-Grafana-Identity-Id"))
require.NoError(t, res.Body.Close())
})
t.Run("should add id header for service account", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.IDResponseHeaderEnabled = true
cfg.IDResponseHeaderPrefix = "X-Grafana"
cfg.IDResponseHeaderNamespaces = map[string]struct{}{"service-account": {}}
res := run(cfg, "service-account:1")
require.Equal(t, "service-account:1", res.Header.Get("X-Grafana-Identity-Id"))
require.NoError(t, res.Body.Close())
})
t.Run("should not add id header for service account when not configured", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.IDResponseHeaderEnabled = true
cfg.IDResponseHeaderPrefix = "X-Grafana"
cfg.IDResponseHeaderNamespaces = map[string]struct{}{"user": {}}
res := run(cfg, "service-account:1")
require.Empty(t, res.Header.Get("X-Grafana-Identity-Id"))
require.NoError(t, res.Body.Close())
})
}) })
} }

View File

@@ -1546,7 +1546,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
// ID response header // ID response header
cfg.IDResponseHeaderEnabled = auth.Key("id_response_header_enabled").MustBool(false) cfg.IDResponseHeaderEnabled = auth.Key("id_response_header_enabled").MustBool(false)
cfg.IDResponseHeaderPrefix = auth.Key("id_response_header_prefix").MustString("X-Grafana-") cfg.IDResponseHeaderPrefix = auth.Key("id_response_header_prefix").MustString("X-Grafana")
idHeaderNamespaces := util.SplitString(auth.Key("id_response_header_namespaces").MustString("")) idHeaderNamespaces := util.SplitString(auth.Key("id_response_header_namespaces").MustString(""))
cfg.IDResponseHeaderNamespaces = make(map[string]struct{}, len(idHeaderNamespaces)) cfg.IDResponseHeaderNamespaces = make(map[string]struct{}, len(idHeaderNamespaces))