mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user