mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-10 01:14:11 -04:00
KEP-5284: add constrained impersonation metrics
See https://kep.k8s.io/5284 for details. apiserver_impersonation_attempts_total{mode, decision} apiserver_impersonation_attempts_duration_seconds{mode, decision} apiserver_impersonation_authorization_attempts_total{mode, decision} apiserver_impersonation_authorization_attempts_duration_seconds{mode, decision} Signed-off-by: Monis Khan <mok@microsoft.com>
This commit is contained in:
parent
ca6034ad9a
commit
ba2a68e1db
5 changed files with 873 additions and 48 deletions
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
|
@ -30,6 +31,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/filters"
|
||||
"k8s.io/apiserver/pkg/endpoints/filters/impersonation/metrics"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
|
|
@ -42,10 +44,20 @@ import (
|
|||
// expression of impersonation access. For example, a service account may be authorized to impersonate the
|
||||
// node that it is associated with but only when listing pods. See the linked KEP for further details.
|
||||
func WithConstrainedImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler {
|
||||
metrics.RegisterMetrics()
|
||||
|
||||
ma := &metricsAuthorizer{
|
||||
delegate: a,
|
||||
recordAuthorizationCall: metrics.RecordImpersonationAuthorizationCall,
|
||||
}
|
||||
|
||||
return &constrainedImpersonationHandler{
|
||||
handler: handler,
|
||||
tracker: newImpersonationModesTracker(a),
|
||||
tracker: newImpersonationModesTracker(ma),
|
||||
s: s,
|
||||
|
||||
recordAttempt: metrics.RecordImpersonationAttempt,
|
||||
metricsAuthorizer: ma,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,6 +65,10 @@ type constrainedImpersonationHandler struct {
|
|||
handler http.Handler
|
||||
tracker *impersonationModesTracker
|
||||
s runtime.NegotiatedSerializer
|
||||
|
||||
// to allow unit tests to override metrics recording
|
||||
recordAttempt func(mode, decision string, duration time.Duration)
|
||||
metricsAuthorizer *metricsAuthorizer
|
||||
}
|
||||
|
||||
func (c *constrainedImpersonationHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
|
@ -78,12 +94,16 @@ func (c *constrainedImpersonationHandler) ServeHTTP(w http.ResponseWriter, req *
|
|||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
impersonatedUser, err := c.tracker.getImpersonatedUser(ctx, wantedUser, attributes)
|
||||
duration := time.Since(start)
|
||||
if err != nil {
|
||||
c.recordAttempt("", "denied", duration)
|
||||
klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "err", err)
|
||||
responsewriters.RespondWithError(w, req, err, c.s)
|
||||
return
|
||||
}
|
||||
c.recordAttempt(modeFromConstraint(impersonatedUser.constraint), "allowed", duration)
|
||||
|
||||
req = req.WithContext(request.WithUser(ctx, impersonatedUser.user))
|
||||
httplog.LogOf(req, w).Addf("%v is impersonating %v", userString(requestor), userString(impersonatedUser.user))
|
||||
|
|
@ -249,3 +269,49 @@ func (t *impersonationModesTracker) getImpersonatedUser(ctx context.Context, wan
|
|||
// this should not happen, but make sure we fail closed when no impersonation mode succeeded
|
||||
return nil, errors.New("all impersonation modes failed")
|
||||
}
|
||||
|
||||
type metricsAuthorizer struct {
|
||||
delegate authorizer.Authorizer
|
||||
recordAuthorizationCall func(mode, decision string, duration time.Duration)
|
||||
}
|
||||
|
||||
func (m *metricsAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
start := time.Now()
|
||||
decision, reason, err := m.delegate.Authorize(ctx, attributes)
|
||||
duration := time.Since(start)
|
||||
|
||||
m.recordAuthorizationCall(modeFromVerb(attributes.GetVerb()), decisionToLabel(decision), duration)
|
||||
|
||||
return decision, reason, err
|
||||
}
|
||||
|
||||
func modeFromConstraint(constraint string) string {
|
||||
if len(constraint) == 0 {
|
||||
return "legacy"
|
||||
}
|
||||
return modeFromVerb(constraint)
|
||||
}
|
||||
|
||||
func modeFromVerb(verb string) string {
|
||||
switch {
|
||||
case verb == "impersonate":
|
||||
return "legacy"
|
||||
case verb == "impersonate:associated-node" || strings.HasPrefix(verb, "impersonate-on:associated-node:"):
|
||||
return "associated-node"
|
||||
case verb == "impersonate:arbitrary-node" || strings.HasPrefix(verb, "impersonate-on:arbitrary-node:"):
|
||||
return "arbitrary-node"
|
||||
case verb == "impersonate:serviceaccount" || strings.HasPrefix(verb, "impersonate-on:serviceaccount:"):
|
||||
return "serviceaccount"
|
||||
case verb == "impersonate:user-info" || strings.HasPrefix(verb, "impersonate-on:user-info:"):
|
||||
return "user-info"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func decisionToLabel(decision authorizer.Decision) string {
|
||||
if decision == authorizer.DecisionAllow {
|
||||
return "allowed"
|
||||
}
|
||||
return "denied"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
|
@ -48,6 +49,10 @@ type constrainedImpersonationTest struct {
|
|||
constrainedImpersonationHandler *constrainedImpersonationHandler
|
||||
checkedAttrs []authorizer.Attributes
|
||||
echoCalled bool
|
||||
|
||||
attemptMode string
|
||||
attemptDecision string
|
||||
authorizationMetrics map[string]int // "mode/decision" -> count
|
||||
}
|
||||
|
||||
func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
|
|
@ -169,6 +174,17 @@ func (c *constrainedImpersonationTest) handler() http.Handler {
|
|||
addImpersonation := WithConstrainedImpersonation(c.echoUserInfoHandler(), c, serializer.NewCodecFactory(s))
|
||||
c.constrainedImpersonationHandler = addImpersonation.(*constrainedImpersonationHandler)
|
||||
|
||||
c.constrainedImpersonationHandler.recordAttempt = func(mode, decision string, _ time.Duration) {
|
||||
c.attemptMode = mode
|
||||
c.attemptDecision = decision
|
||||
}
|
||||
c.constrainedImpersonationHandler.metricsAuthorizer.recordAuthorizationCall = func(mode, decision string, _ time.Duration) {
|
||||
if c.authorizationMetrics == nil {
|
||||
c.authorizationMetrics = map[string]int{}
|
||||
}
|
||||
c.authorizationMetrics[mode+"/"+decision]++
|
||||
}
|
||||
|
||||
addAuthentication := c.authenticationHandler(addImpersonation)
|
||||
addRequestInfo := c.requestInfoHandler(addAuthentication)
|
||||
return addRequestInfo
|
||||
|
|
@ -249,6 +265,21 @@ func (c *constrainedImpersonationTest) assertEchoCalled(expectedCalled bool) {
|
|||
require.Equal(c.t, expectedCalled, called)
|
||||
}
|
||||
|
||||
func (c *constrainedImpersonationTest) assertMetrics(r testRequest) {
|
||||
c.t.Helper()
|
||||
|
||||
attemptMode := c.attemptMode
|
||||
attemptDecision := c.attemptDecision
|
||||
authorizationMetrics := c.authorizationMetrics
|
||||
c.attemptMode = ""
|
||||
c.attemptDecision = ""
|
||||
c.authorizationMetrics = nil
|
||||
|
||||
require.Equal(c.t, r.expectedAttemptMode, attemptMode, "unexpected attempt mode")
|
||||
require.Equal(c.t, r.expectedAttemptDecision, attemptDecision, "unexpected attempt decision")
|
||||
require.Equal(c.t, r.expectedAuthorizationMetrics, authorizationMetrics, "unexpected authorization metrics")
|
||||
}
|
||||
|
||||
func (c *constrainedImpersonationTest) assertCache(r testRequest) {
|
||||
rr := require.New(c.t)
|
||||
|
||||
|
|
@ -380,6 +411,10 @@ type testRequest struct {
|
|||
expectedAttributes []authorizer.AttributesRecord
|
||||
expectedCache *expectedCache
|
||||
expectedCode int
|
||||
|
||||
expectedAttemptMode string
|
||||
expectedAttemptDecision string
|
||||
expectedAuthorizationMetrics map[string]int
|
||||
}
|
||||
|
||||
func associatedNodeTestCase() []testRequest {
|
||||
|
|
@ -470,18 +505,26 @@ func associatedNodeTestCase() []testRequest {
|
|||
withImpersonateOnAttributes(getSecretRequest, "associated-node"),
|
||||
withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "*"}, "associated-node"),
|
||||
},
|
||||
expectedCache: cacheWithOnlyNode1Data,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCache: cacheWithOnlyNode1Data,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "associated-node",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"associated-node/allowed": 2, // impersonate-on + impersonate:associated-node
|
||||
},
|
||||
},
|
||||
{
|
||||
request: getSecretRequest,
|
||||
requestor: saDefaultOnNode2,
|
||||
impersonatedUser: &user.DefaultInfo{Name: "system:node:node2"}, // node matches
|
||||
expectedImpersonatedUser: node2FullUserInfo,
|
||||
expectedAttributesUser: saDefaultOnAnyNode,
|
||||
expectedAttributes: nil, // no authz checks for the second request
|
||||
expectedCache: cacheWithOnlyNode1Data,
|
||||
expectedCode: http.StatusOK,
|
||||
request: getSecretRequest,
|
||||
requestor: saDefaultOnNode2,
|
||||
impersonatedUser: &user.DefaultInfo{Name: "system:node:node2"}, // node matches
|
||||
expectedImpersonatedUser: node2FullUserInfo,
|
||||
expectedAttributesUser: saDefaultOnAnyNode,
|
||||
expectedAttributes: nil, // no authz checks for the second request
|
||||
expectedCache: cacheWithOnlyNode1Data,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "associated-node",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: nil, // full cache hit
|
||||
},
|
||||
{
|
||||
request: getSecretRequest,
|
||||
|
|
@ -494,8 +537,15 @@ func associatedNodeTestCase() []testRequest {
|
|||
withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"),
|
||||
withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}),
|
||||
},
|
||||
expectedCache: cacheWithOnlyNode1Data,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedCache: cacheWithOnlyNode1Data,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"arbitrary-node/allowed": 1, // impersonate-on:arbitrary-node:get
|
||||
"arbitrary-node/denied": 1, // impersonate:arbitrary-node nodes
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
{
|
||||
request: getPodRequest,
|
||||
|
|
@ -506,18 +556,26 @@ func associatedNodeTestCase() []testRequest {
|
|||
expectedAttributes: []authorizer.AttributesRecord{
|
||||
withImpersonateOnAttributes(getPodRequest, "associated-node"), // one authz check because different request info
|
||||
},
|
||||
expectedCache: cacheWithMultipleRequests,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCache: cacheWithMultipleRequests,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "associated-node",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"associated-node/allowed": 1, // impersonate-on only (inner cache hit)
|
||||
},
|
||||
},
|
||||
{
|
||||
request: getPodRequest,
|
||||
requestor: saDefaultOnNode1,
|
||||
impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node matches
|
||||
expectedImpersonatedUser: node1FullUserInfo,
|
||||
expectedAttributesUser: nil,
|
||||
expectedAttributes: nil, // no authz checks for pod request via node1
|
||||
expectedCache: cacheWithMultipleRequests,
|
||||
expectedCode: http.StatusOK,
|
||||
request: getPodRequest,
|
||||
requestor: saDefaultOnNode1,
|
||||
impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node matches
|
||||
expectedImpersonatedUser: node1FullUserInfo,
|
||||
expectedAttributesUser: nil,
|
||||
expectedAttributes: nil, // no authz checks for pod request via node1
|
||||
expectedCache: cacheWithMultipleRequests,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "associated-node",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: nil, // full cache hit
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -592,9 +650,16 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info"),
|
||||
withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}),
|
||||
},
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `users.authentication.k8s.io "anyone" is forbidden: User "tester" cannot impersonate:user-info resource "users" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `users.authentication.k8s.io "anyone" is forbidden: User "tester" cannot impersonate:user-info resource "users" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 1, // impersonate-on:user-info:get
|
||||
"user-info/denied": 1, // impersonate:user-info users
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -625,7 +690,12 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "user-info",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 2, // impersonate-on + impersonate:user-info
|
||||
},
|
||||
},
|
||||
{
|
||||
request: getAnotherPodRequest,
|
||||
|
|
@ -651,7 +721,12 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "user-info",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 1, // impersonate-on (inner cache hit skips impersonate:user-info)
|
||||
},
|
||||
},
|
||||
{
|
||||
request: createPodRequest,
|
||||
|
|
@ -677,8 +752,14 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `pods "foo" is forbidden: User "user-impersonater" cannot impersonate-on:user-info:create resource "pods" in API group "" in the namespace "bar": deny by default`,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `pods "foo" is forbidden: User "user-impersonater" cannot impersonate-on:user-info:create resource "pods" in API group "" in the namespace "bar": deny by default`,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/denied": 1, // impersonate-on:user-info:create
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -712,7 +793,12 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "serviceaccount",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"serviceaccount/allowed": 2, // impersonate-on + impersonate:serviceaccount
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -728,9 +814,16 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"),
|
||||
withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}),
|
||||
},
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "sa-impersonater" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "sa-impersonater" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"arbitrary-node/allowed": 1, // impersonate-on:arbitrary-node:get
|
||||
"arbitrary-node/denied": 1, // impersonate:arbitrary-node nodes
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -745,9 +838,15 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
withImpersonateOnAttributes(createPodRequest, "arbitrary-node"),
|
||||
withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}),
|
||||
},
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `pods "foo" is forbidden: User "node-impersonater" cannot impersonate-on:arbitrary-node:create resource "pods" in API group "" in the namespace "bar": deny by default`,
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `pods "foo" is forbidden: User "node-impersonater" cannot impersonate-on:arbitrary-node:create resource "pods" in API group "" in the namespace "bar": deny by default`,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"arbitrary-node/denied": 1, // impersonate-on:arbitrary-node:create
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -778,7 +877,12 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "arbitrary-node",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"arbitrary-node/allowed": 2, // impersonate-on + impersonate:arbitrary-node
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -803,9 +907,16 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info"),
|
||||
withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}),
|
||||
},
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `userextras.authentication.k8s.io "scope-a" is forbidden: User "user-impersonater" cannot impersonate:user-info resource "userextras/pandas.io/scopes" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `userextras.authentication.k8s.io "scope-a" is forbidden: User "user-impersonater" cannot impersonate:user-info resource "userextras/pandas.io/scopes" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 3, // impersonate-on:get + users + groups
|
||||
"user-info/denied": 1, // userextras scope-a
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -880,7 +991,13 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "user-info",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 9, // impersonate-on:get + users + 7 individual extra values
|
||||
"user-info/denied": 1, // wildcard userextras check
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -906,9 +1023,16 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node"),
|
||||
withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"}),
|
||||
},
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "user-impersonater" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedCache: nil,
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "user-impersonater" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`,
|
||||
expectedAttemptMode: "",
|
||||
expectedAttemptDecision: "denied",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"arbitrary-node/allowed": 1, // impersonate-on:arbitrary-node:get
|
||||
"arbitrary-node/denied": 1, // impersonate:arbitrary-node nodes
|
||||
"legacy/denied": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -934,7 +1058,14 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
modes: nil, // legacy impersonation does not cache
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "legacy",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 1, // impersonate-on:user-info:get
|
||||
"user-info/denied": 1, // impersonate:user-info users
|
||||
"legacy/allowed": 1, // impersonate users
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -974,7 +1105,12 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedAttemptMode: "user-info",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: map[string]int{
|
||||
"user-info/allowed": 2, // impersonate-on + impersonate:user-info
|
||||
},
|
||||
},
|
||||
{
|
||||
request: getDeploymentRequest,
|
||||
|
|
@ -1006,6 +1142,9 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
expectedAttemptMode: "user-info",
|
||||
expectedAttemptDecision: "allowed",
|
||||
expectedAuthorizationMetrics: nil, // full cache hit
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1082,6 +1221,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
|
|||
|
||||
test.assertAttributes(r)
|
||||
test.assertCache(r)
|
||||
test.assertMetrics(r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "apiserver"
|
||||
subsystem = "impersonation"
|
||||
)
|
||||
|
||||
var (
|
||||
impersonationAttemptsTotal = metrics.NewCounterVec(
|
||||
&metrics.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "attempts_total",
|
||||
Help: "Total number of impersonation attempts split by mode and decision.",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{"mode", "decision"},
|
||||
)
|
||||
|
||||
impersonationAttemptsDurationSeconds = metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "attempts_duration_seconds",
|
||||
Help: "Latency of impersonation attempts in seconds split by mode and decision.",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
|
||||
},
|
||||
[]string{"mode", "decision"},
|
||||
)
|
||||
|
||||
impersonationAuthorizationAttemptsTotal = metrics.NewCounterVec(
|
||||
&metrics.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "authorization_attempts_total",
|
||||
Help: "Total number of authorization checks made by the impersonation handler split by mode and decision.",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{"mode", "decision"},
|
||||
)
|
||||
|
||||
impersonationAuthorizationAttemptsDurationSeconds = metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "authorization_attempts_duration_seconds",
|
||||
Help: "Latency of authorization checks made by the impersonation handler in seconds split by mode and decision.",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
|
||||
},
|
||||
[]string{"mode", "decision"},
|
||||
)
|
||||
)
|
||||
|
||||
var registerMetrics sync.Once
|
||||
|
||||
func RegisterMetrics() {
|
||||
registerMetrics.Do(func() {
|
||||
legacyregistry.MustRegister(impersonationAttemptsTotal)
|
||||
legacyregistry.MustRegister(impersonationAttemptsDurationSeconds)
|
||||
legacyregistry.MustRegister(impersonationAuthorizationAttemptsTotal)
|
||||
legacyregistry.MustRegister(impersonationAuthorizationAttemptsDurationSeconds)
|
||||
})
|
||||
}
|
||||
|
||||
func RecordImpersonationAttempt(mode, decision string, duration time.Duration) {
|
||||
impersonationAttemptsTotal.WithLabelValues(mode, decision).Inc()
|
||||
impersonationAttemptsDurationSeconds.WithLabelValues(mode, decision).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
func RecordImpersonationAuthorizationCall(mode, decision string, duration time.Duration) {
|
||||
impersonationAuthorizationAttemptsTotal.WithLabelValues(mode, decision).Inc()
|
||||
impersonationAuthorizationAttemptsDurationSeconds.WithLabelValues(mode, decision).Observe(duration.Seconds())
|
||||
}
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
)
|
||||
|
||||
func TestRecordImpersonationAttempt(t *testing.T) {
|
||||
RegisterMetrics()
|
||||
|
||||
attemptMetrics := []string{
|
||||
namespace + "_" + subsystem + "_attempts_total",
|
||||
namespace + "_" + subsystem + "_attempts_duration_seconds",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
mode string
|
||||
decision string
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
name: "allowed with user-info mode",
|
||||
mode: "user-info",
|
||||
decision: "allowed",
|
||||
expectedValue: `
|
||||
# HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_total counter
|
||||
apiserver_impersonation_attempts_total{decision="allowed",mode="user-info"} 1
|
||||
# HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.1
|
||||
apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "denied attempt",
|
||||
mode: "",
|
||||
decision: "denied",
|
||||
expectedValue: `
|
||||
# HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_total counter
|
||||
apiserver_impersonation_attempts_total{decision="denied",mode=""} 1
|
||||
# HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.001"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.002"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.004"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.008"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.016"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.032"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.064"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.128"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.256"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.512"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="1.024"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="2.048"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="4.096"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="8.192"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="16.384"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="+Inf"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} 0.1
|
||||
apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "allowed with legacy mode",
|
||||
mode: "legacy",
|
||||
decision: "allowed",
|
||||
expectedValue: `
|
||||
# HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_total counter
|
||||
apiserver_impersonation_attempts_total{decision="allowed",mode="legacy"} 1
|
||||
# HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.001"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.002"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.004"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.008"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.016"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.032"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.064"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.128"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.256"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.512"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="1.024"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="2.048"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="4.096"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="8.192"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="16.384"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="+Inf"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} 0.1
|
||||
apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resetMetricsForTest()
|
||||
|
||||
RecordImpersonationAttempt(tc.mode, tc.decision, 100*time.Millisecond)
|
||||
|
||||
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedValue), attemptMetrics...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordImpersonationAuthorizationCall(t *testing.T) {
|
||||
RegisterMetrics()
|
||||
|
||||
authorizationMetrics := []string{
|
||||
namespace + "_" + subsystem + "_authorization_attempts_total",
|
||||
namespace + "_" + subsystem + "_authorization_attempts_duration_seconds",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
mode string
|
||||
decision string
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
name: "user-info allowed",
|
||||
mode: "user-info",
|
||||
decision: "allowed",
|
||||
expectedValue: `
|
||||
# HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_total counter
|
||||
apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="user-info"} 1
|
||||
# HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "arbitrary-node denied",
|
||||
mode: "arbitrary-node",
|
||||
decision: "denied",
|
||||
expectedValue: `
|
||||
# HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_total counter
|
||||
apiserver_impersonation_authorization_attempts_total{decision="denied",mode="arbitrary-node"} 1
|
||||
# HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.001"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.002"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.004"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.008"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.016"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.032"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.064"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.128"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.256"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.512"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="1.024"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="2.048"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="4.096"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="8.192"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="16.384"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="+Inf"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} 0.1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="arbitrary-node"} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "legacy allowed",
|
||||
mode: "legacy",
|
||||
decision: "allowed",
|
||||
expectedValue: `
|
||||
# HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_total counter
|
||||
apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="legacy"} 1
|
||||
# HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.001"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.002"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.004"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.008"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.016"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.032"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.064"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.128"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.256"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.512"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="1.024"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="2.048"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="4.096"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="8.192"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="16.384"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="+Inf"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} 0.1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resetMetricsForTest()
|
||||
|
||||
RecordImpersonationAuthorizationCall(tc.mode, tc.decision, 100*time.Millisecond)
|
||||
|
||||
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedValue), authorizationMetrics...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordImpersonationMetricsMultiple(t *testing.T) {
|
||||
RegisterMetrics()
|
||||
resetMetricsForTest()
|
||||
|
||||
RecordImpersonationAttempt("user-info", "allowed", 100*time.Millisecond)
|
||||
RecordImpersonationAttempt("", "denied", 50*time.Millisecond)
|
||||
RecordImpersonationAttempt("", "denied", 50*time.Millisecond)
|
||||
RecordImpersonationAuthorizationCall("user-info", "allowed", 100*time.Millisecond)
|
||||
RecordImpersonationAuthorizationCall("user-info", "allowed", 100*time.Millisecond)
|
||||
RecordImpersonationAuthorizationCall("user-info", "denied", 50*time.Millisecond)
|
||||
RecordImpersonationAuthorizationCall("legacy", "denied", 50*time.Millisecond)
|
||||
|
||||
expectedValue := `
|
||||
# HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.1
|
||||
apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.001"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.002"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.004"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.008"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.016"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.032"} 0
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.064"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.128"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.256"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.512"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="1.024"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="2.048"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="4.096"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="8.192"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="16.384"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="+Inf"} 2
|
||||
apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} 0.1
|
||||
apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 2
|
||||
# HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision.
|
||||
# TYPE apiserver_impersonation_attempts_total counter
|
||||
apiserver_impersonation_attempts_total{decision="allowed",mode="user-info"} 1
|
||||
apiserver_impersonation_attempts_total{decision="denied",mode=""} 2
|
||||
# HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 2
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.001"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.002"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.004"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.008"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.016"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.032"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.064"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.128"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.256"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.512"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="1.024"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="2.048"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="4.096"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="8.192"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="16.384"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="+Inf"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} 0.05
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.001"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.002"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.004"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.008"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.016"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.032"} 0
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.064"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.128"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.256"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.512"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="1.024"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="2.048"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="4.096"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="8.192"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="16.384"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="+Inf"} 1
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} 0.05
|
||||
apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="user-info"} 1
|
||||
# HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision.
|
||||
# TYPE apiserver_impersonation_authorization_attempts_total counter
|
||||
apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="user-info"} 2
|
||||
apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 1
|
||||
apiserver_impersonation_authorization_attempts_total{decision="denied",mode="user-info"} 1
|
||||
`
|
||||
|
||||
allMetrics := []string{
|
||||
namespace + "_" + subsystem + "_attempts_duration_seconds",
|
||||
namespace + "_" + subsystem + "_attempts_total",
|
||||
namespace + "_" + subsystem + "_authorization_attempts_duration_seconds",
|
||||
namespace + "_" + subsystem + "_authorization_attempts_total",
|
||||
}
|
||||
|
||||
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), allMetrics...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func resetMetricsForTest() {
|
||||
impersonationAttemptsTotal.Reset()
|
||||
impersonationAttemptsDurationSeconds.Reset()
|
||||
impersonationAuthorizationAttemptsTotal.Reset()
|
||||
impersonationAuthorizationAttemptsDurationSeconds.Reset()
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -1110,6 +1112,8 @@ func TestConstrainedImpersonation(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("bob impersonating alice", func(t *testing.T) {
|
||||
resetAllMetrics(t, ctx, superuserClient)
|
||||
|
||||
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
|
||||
impersonatorClientConfig.BearerToken = "bob"
|
||||
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
|
||||
|
|
@ -1164,9 +1168,29 @@ func TestConstrainedImpersonation(t *testing.T) {
|
|||
if !errors.IsForbidden(err) {
|
||||
t.Fatalf("expected forbidden error, got %T %v", err, err)
|
||||
}
|
||||
|
||||
assertImpersonationMetrics(t, ctx, superuserClient, []string{
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 3`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} FP`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} FP`,
|
||||
`apiserver_impersonation_attempts_total{decision="allowed",mode="user-info"} 1`,
|
||||
`apiserver_impersonation_attempts_total{decision="denied",mode=""} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="user-info"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="user-info"} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="user-info"} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} FP`,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("bob impersonating a node", func(t *testing.T) {
|
||||
resetAllMetrics(t, ctx, superuserClient)
|
||||
|
||||
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
|
||||
impersonatorClientConfig.BearerToken = "bob"
|
||||
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
|
||||
|
|
@ -1221,9 +1245,29 @@ func TestConstrainedImpersonation(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("expected no error, got %T %v", err, err)
|
||||
}
|
||||
|
||||
assertImpersonationMetrics(t, ctx, superuserClient, []string{
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="arbitrary-node"} 1`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 2`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="arbitrary-node"} FP`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} FP`,
|
||||
`apiserver_impersonation_attempts_total{decision="allowed",mode="arbitrary-node"} 1`,
|
||||
`apiserver_impersonation_attempts_total{decision="denied",mode=""} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="arbitrary-node"} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="arbitrary-node"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="arbitrary-node"} 3`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="arbitrary-node"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="arbitrary-node"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("impersonating scheduled node", func(t *testing.T) {
|
||||
resetAllMetrics(t, ctx, superuserClient)
|
||||
|
||||
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
|
||||
impersonatorClientConfig.BearerToken = "serviceaccount2"
|
||||
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
|
||||
|
|
@ -1275,9 +1319,29 @@ func TestConstrainedImpersonation(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("expected no error, got %T %v", err, err)
|
||||
}
|
||||
|
||||
assertImpersonationMetrics(t, ctx, superuserClient, []string{
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="associated-node"} 1`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 1`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="associated-node"} FP`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} FP`,
|
||||
`apiserver_impersonation_attempts_total{decision="allowed",mode="associated-node"} 1`,
|
||||
`apiserver_impersonation_attempts_total{decision="denied",mode=""} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="associated-node"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="arbitrary-node"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="associated-node"} 2`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="arbitrary-node"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="associated-node"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("fallback to legacy impersonation", func(t *testing.T) {
|
||||
resetAllMetrics(t, ctx, superuserClient)
|
||||
|
||||
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
|
||||
impersonatorClientConfig.BearerToken = "bob"
|
||||
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
|
||||
|
|
@ -1293,13 +1357,68 @@ func TestConstrainedImpersonation(t *testing.T) {
|
|||
Resources: []string{"users"},
|
||||
})
|
||||
|
||||
_, err := client.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{})
|
||||
_, err := client.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "app=panda", // force this request to have a different cache key than the earlier test
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %T %v", err, err)
|
||||
}
|
||||
|
||||
assertImpersonationMetrics(t, ctx, superuserClient, []string{
|
||||
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1`,
|
||||
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} FP`,
|
||||
`apiserver_impersonation_attempts_total{decision="allowed",mode="legacy"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="legacy"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="user-info"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="user-info"} 1`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} FP`,
|
||||
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} FP`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func resetAllMetrics(t *testing.T, ctx context.Context, client clientset.Interface) {
|
||||
t.Helper()
|
||||
|
||||
if err := client.CoreV1().RESTClient().Delete().AbsPath("/metrics").Do(ctx).Error(); err != nil {
|
||||
t.Fatalf("failed to reset metrics: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertImpersonationMetrics(t *testing.T, ctx context.Context, client clientset.Interface, wantMetricStrings []string) {
|
||||
t.Helper()
|
||||
|
||||
rc := client.CoreV1().RESTClient()
|
||||
|
||||
body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to fetch metrics: %v", err)
|
||||
}
|
||||
|
||||
var gotMetricStrings []string
|
||||
trimFP := regexp.MustCompile(`(.*)(} \d+\.\d+.*)`)
|
||||
for line := range strings.SplitSeq(string(body), "\n") {
|
||||
if !strings.HasPrefix(line, "apiserver_impersonation_") {
|
||||
continue
|
||||
}
|
||||
// skip histogram bucket lines to keep assertions manageable
|
||||
if strings.Contains(line, "_bucket{") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "_seconds_sum") {
|
||||
line = trimFP.ReplaceAllString(line, `$1`) + "} FP"
|
||||
}
|
||||
gotMetricStrings = append(gotMetricStrings, line)
|
||||
}
|
||||
slices.Sort(gotMetricStrings)
|
||||
slices.Sort(wantMetricStrings)
|
||||
|
||||
if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" {
|
||||
t.Errorf("unexpected impersonation metrics diff (-want +got): %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConstrainedImpersonationDisabled tests the impersonation behavior when the
|
||||
// ConstrainedImpersonation feature gate is disabled. In this mode, the legacy
|
||||
// impersonation behavior is expected, where a user only needs the "impersonate"
|
||||
|
|
|
|||
Loading…
Reference in a new issue