mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Add selector authorization to the Node authorizer
This commit is contained in:
parent
a1398a8cca
commit
4d535db8be
@ -70,6 +70,13 @@ func validateAPIPriorityAndFairness(options *Options) []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNodeSelectorAuthorizationFeature() []error {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.AuthorizeNodeWithSelectors) && !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
|
||||
return []error{fmt.Errorf("AuthorizeNodeWithSelectors feature requires AuthorizeWithSelectors feature to be enabled")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUnknownVersionInteroperabilityProxyFeature() []error {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) {
|
||||
@ -113,6 +120,7 @@ func (s *Options) Validate() []error {
|
||||
errs = append(errs, s.Metrics.Validate()...)
|
||||
errs = append(errs, validateUnknownVersionInteroperabilityProxyFeature()...)
|
||||
errs = append(errs, validateUnknownVersionInteroperabilityProxyFlags(s)...)
|
||||
errs = append(errs, validateNodeSelectorAuthorizationFeature()...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
@ -74,6 +74,14 @@ const (
|
||||
// beta: v1.30
|
||||
AppArmorFields featuregate.Feature = "AppArmorFields"
|
||||
|
||||
// owner: @liggitt
|
||||
// kep:
|
||||
// alpha: v1.31
|
||||
//
|
||||
// Make the Node authorizer use fine-grained selector authorization.
|
||||
// Requires AuthorizeWithSelectors to be enabled.
|
||||
AuthorizeNodeWithSelectors featuregate.Feature = "AuthorizeNodeWithSelectors"
|
||||
|
||||
// owner: @danwinship
|
||||
// alpha: v1.27
|
||||
// beta: v1.29
|
||||
@ -1008,6 +1016,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
AppArmorFields: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
AuthorizeNodeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
CloudDualStackNodeIPs: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
|
||||
|
||||
ClusterTrustBundle: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
@ -33,6 +34,7 @@ import (
|
||||
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
|
||||
storageapi "k8s.io/kubernetes/pkg/apis/storage"
|
||||
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
|
||||
"k8s.io/kubernetes/third_party/forked/gonum/graph"
|
||||
"k8s.io/kubernetes/third_party/forked/gonum/graph/traverse"
|
||||
@ -81,6 +83,8 @@ func NewAuthorizer(graph *Graph, identifier nodeidentifier.NodeIdentifier, rules
|
||||
var (
|
||||
configMapResource = api.Resource("configmaps")
|
||||
secretResource = api.Resource("secrets")
|
||||
podResource = api.Resource("pods")
|
||||
nodeResource = api.Resource("nodes")
|
||||
resourceSlice = resourceapi.Resource("resourceslices")
|
||||
pvcResource = api.Resource("persistentvolumeclaims")
|
||||
pvResource = api.Resource("persistentvolumes")
|
||||
@ -138,8 +142,15 @@ func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attribu
|
||||
return r.authorizeCSINode(nodeName, attrs)
|
||||
case resourceSlice:
|
||||
return r.authorizeResourceSlice(nodeName, attrs)
|
||||
case nodeResource:
|
||||
if r.features.Enabled(features.AuthorizeNodeWithSelectors) {
|
||||
return r.authorizeNode(nodeName, attrs)
|
||||
}
|
||||
case podResource:
|
||||
if r.features.Enabled(features.AuthorizeNodeWithSelectors) {
|
||||
return r.authorizePod(nodeName, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Access to other resources is not subdivided, so just evaluate against the statically defined node rules
|
||||
@ -323,22 +334,104 @@ func (r *NodeAuthorizer) authorizeResourceSlice(nodeName string, attrs authorize
|
||||
// is allowed by recording a graph edge.
|
||||
return r.authorize(nodeName, sliceVertexType, attrs)
|
||||
case "watch", "list", "deletecollection":
|
||||
// Okay. The kubelet is trusted to use a filter for its own objects in watch and list.
|
||||
// The NodeRestriction admission plugin (plugin/pkg/admission/noderestriction)
|
||||
// ensures that the node is not deleting some ResourceSlice belonging to
|
||||
// some other node.
|
||||
//
|
||||
// TODO (https://github.com/kubernetes/kubernetes/issues/125355):
|
||||
// Once https://github.com/kubernetes/enhancements/pull/4600 is implemented,
|
||||
// this code needs to be extended to verify that the node filter is indeed set.
|
||||
// Then the admission check can be removed.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
if r.features.Enabled(features.AuthorizeNodeWithSelectors) {
|
||||
// only allow a scoped fieldSelector
|
||||
reqs, _ := attrs.GetFieldSelector()
|
||||
for _, req := range reqs {
|
||||
if req.Field == "nodeName" && req.Operator == selection.Equals && req.Value == nodeName {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
}
|
||||
// deny otherwise
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "can only list/watch/deletecollection resourceslices with nodeName field selector", nil
|
||||
} else {
|
||||
// Allow broad list/watch access if AuthorizeNodeWithSelectors is not enabled.
|
||||
//
|
||||
// The NodeRestriction admission plugin (plugin/pkg/admission/noderestriction)
|
||||
// ensures that the node is not deleting some ResourceSlice belonging to
|
||||
// some other node.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
default:
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "only the following verbs are allowed for a ResourceSlice: get, watch, list, create, update, patch, delete, deletecollection", nil
|
||||
}
|
||||
}
|
||||
|
||||
// authorizeNode authorizes node requests to Node API objects
|
||||
func (r *NodeAuthorizer) authorizeNode(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
switch attrs.GetSubresource() {
|
||||
case "":
|
||||
switch attrs.GetVerb() {
|
||||
case "create", "update", "patch":
|
||||
// Use the NodeRestriction admission plugin to limit a node to creating/updating its own API object.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
case "get", "list", "watch":
|
||||
return r.authorize(nodeName, nodeVertexType, attrs)
|
||||
}
|
||||
case "status":
|
||||
switch attrs.GetVerb() {
|
||||
case "update", "patch":
|
||||
// Use the NodeRestriction admission plugin to limit a node to updating its own Node status.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// authorizePod authorizes node requests to Pod API objects
|
||||
func (r *NodeAuthorizer) authorizePod(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
switch attrs.GetSubresource() {
|
||||
case "":
|
||||
switch attrs.GetVerb() {
|
||||
case "get":
|
||||
return r.authorizeGet(nodeName, podVertexType, attrs)
|
||||
|
||||
case "list", "watch":
|
||||
// allow a scoped fieldSelector
|
||||
reqs, _ := attrs.GetFieldSelector()
|
||||
for _, req := range reqs {
|
||||
if req.Field == "spec.nodeName" && req.Operator == selection.Equals && req.Value == nodeName {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
}
|
||||
// allow a read of a single pod known to be related to the node
|
||||
if attrs.GetName() != "" {
|
||||
return r.authorize(nodeName, podVertexType, attrs)
|
||||
}
|
||||
// deny otherwise
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "can only list/watch pods with spec.nodeName field selector", nil
|
||||
|
||||
case "create", "delete":
|
||||
// Needed for the node to create/delete mirror pods.
|
||||
// Use the NodeRestriction admission plugin to limit a node to creating/deleting mirror pods bound to itself.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
case "status":
|
||||
switch attrs.GetVerb() {
|
||||
case "update", "patch":
|
||||
// Needed for the node to report status of pods it is running.
|
||||
// Use the NodeRestriction admission plugin to limit a node to updating status of pods bound to itself.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
case "eviction":
|
||||
if attrs.GetVerb() == "create" {
|
||||
// Needed for the node to evict pods it is running.
|
||||
// Use the NodeRestriction admission plugin to limit a node to evicting pods bound to itself.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node
|
||||
func (r *NodeAuthorizer) hasPathFrom(nodeName string, startingType vertexType, startingNamespace, startingName string) (bool, error) {
|
||||
r.graph.lock.RLock()
|
||||
|
@ -31,15 +31,19 @@ import (
|
||||
resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/component-base/featuregate"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy"
|
||||
)
|
||||
|
||||
func TestAuthorizer(t *testing.T) {
|
||||
func TestNodeAuthorizer(t *testing.T) {
|
||||
g := NewGraph()
|
||||
|
||||
opts := &sampleDataOpts{
|
||||
@ -66,6 +70,22 @@ func TestAuthorizer(t *testing.T) {
|
||||
|
||||
node0 := &user.DefaultInfo{Name: "system:node:node0", Groups: []string{"system:nodes"}}
|
||||
|
||||
selectorAuthzDisabled := utilfeature.DefaultFeatureGate.DeepCopy()
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzDisabled, genericfeatures.AuthorizeWithSelectors, false)
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzDisabled, features.AuthorizeNodeWithSelectors, false)
|
||||
|
||||
selectorAuthzEnabled := utilfeature.DefaultFeatureGate.DeepCopy()
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzEnabled, genericfeatures.AuthorizeWithSelectors, true)
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzEnabled, features.AuthorizeNodeWithSelectors, true)
|
||||
|
||||
featureVariants := []struct {
|
||||
suffix string
|
||||
features featuregate.FeatureGate
|
||||
}{
|
||||
{suffix: "selector_disabled", features: selectorAuthzDisabled},
|
||||
{suffix: "selector_enabled", features: selectorAuthzEnabled},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attrs authorizer.AttributesRecord
|
||||
@ -365,15 +385,39 @@ func TestAuthorizer(t *testing.T) {
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "allowed list ResourceSlices",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
name: "allowed filtered list ResourceSlices",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io", FieldSelectorRequirements: mustParseFields("nodeName==node0")},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed watch ResourceSlices",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
name: "allowed unfiltered list ResourceSlices - selector authz disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "disallowed unfiltered list ResourceSlices - selector authz enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
{
|
||||
name: "allowed filtered watch ResourceSlices",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io", FieldSelectorRequirements: mustParseFields("nodeName==node0")},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed unfiltered watch ResourceSlices - selector authz disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "disallowed unfiltered watch ResourceSlices - selector authz enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
{
|
||||
name: "allowed get ResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
@ -399,24 +443,299 @@ func TestAuthorizer(t *testing.T) {
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
|
||||
// pods
|
||||
// get pods
|
||||
{
|
||||
name: "get related pod",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "pods", APIGroup: "", Name: "pod0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "get unrelated pod - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "pods", APIGroup: "", Name: "pod0-node1", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "get unrelated pod - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "pods", APIGroup: "", Name: "pod0-node1", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
// list pods
|
||||
{
|
||||
name: "list related pods",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: "", FieldSelectorRequirements: mustParseFields("spec.nodeName=node0")},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "list related pods - alternate selector",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: "", FieldSelectorRequirements: mustParseFields("spec.nodeName==node0")},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "list single related pod",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: "", Name: "pod0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "list unrelated pods - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "list unrelated pods - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
// watch pods
|
||||
{
|
||||
name: "watch related pods",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: "", FieldSelectorRequirements: mustParseFields("spec.nodeName=node0")},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "watch related pods",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: "", FieldSelectorRequirements: mustParseFields("spec.nodeName==node0")},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "watch single related pod",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: "", Name: "pod0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "watch unrelated pods - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "watch unrelated pods - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
// create, delete pods
|
||||
{
|
||||
name: "create unnamed pod",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "pods", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "create pod proxy subresource forbidden",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "pods", Name: "", Subresource: "proxy", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "delete related pod",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "pods", Name: "pod0-node0", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "delete unrelated pod",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "pods", Name: "pod0-node1", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
{
|
||||
name: "delete pod proxy subresource forbidden",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "pods", Name: "pod0-node0", Subresource: "proxy", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
// update/patch pod status
|
||||
{
|
||||
name: "update related pod status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "pods", Subresource: "status", Name: "pod0-node0", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "update unrelated pod status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "pods", Subresource: "status", Name: "pod0-node1", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
{
|
||||
name: "patch related pod status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "pods", Subresource: "status", Name: "pod0-node0", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "patch unrelated pod status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "pods", Subresource: "status", Name: "pod0-node1", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
{
|
||||
name: "get pod status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "status", Name: "pod0-node0", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion, // get status not allowed before or after selector feature
|
||||
},
|
||||
// create pod eviction
|
||||
{
|
||||
name: "create related pod eviction",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "pods", Subresource: "eviction", Name: "pod0-node0", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "create unrelated pod eviction",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "pods", Subresource: "eviction", Name: "pod0-node1", APIGroup: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
|
||||
// nodes
|
||||
// get nodes
|
||||
{
|
||||
name: "get related node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "nodes", APIGroup: "", Name: "node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "get unrelated node - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "get unrelated pod - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
// list nodes
|
||||
{
|
||||
name: "list single related node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: "", Name: "node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "list single unrelated node - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "list single unrelated node - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
{
|
||||
name: "list all nodes - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "list all nodes - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
// watch nodes
|
||||
{
|
||||
name: "watch single related node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: "", Name: "node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "watch single unrelated node - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "watch single unrelated node - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
{
|
||||
name: "watch all nodes - selector disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "watch all nodes - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
},
|
||||
// create nodes
|
||||
{
|
||||
name: "create node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
// update/patch nodes
|
||||
{
|
||||
name: "update related node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "nodes", Name: "node0", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "update unrelated node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "nodes", Name: "node1", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
{
|
||||
name: "patch related node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "nodes", Name: "node0", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "patch unrelated node",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "nodes", Name: "node1", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
// update/patch node status
|
||||
{
|
||||
name: "update related node status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "nodes", Subresource: "status", Name: "node0", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "update unrelated node status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "nodes", Subresource: "status", Name: "node1", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
{
|
||||
name: "patch related node status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "nodes", Subresource: "status", Name: "node0", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "patch unrelated node status",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "nodes", Subresource: "status", Name: "node1", APIGroup: ""},
|
||||
expect: authorizer.DecisionAllow, // guarded by NodeRestriction admission
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.features == nil {
|
||||
authz.features = utilfeature.DefaultFeatureGate
|
||||
} else {
|
||||
if tc.features == nil {
|
||||
for _, variant := range featureVariants {
|
||||
t.Run(tc.name+"_"+variant.suffix, func(t *testing.T) {
|
||||
authz.features = variant.features
|
||||
decision, reason, _ := authz.Authorize(context.Background(), tc.attrs)
|
||||
if decision != tc.expect {
|
||||
t.Errorf("expected %v, got %v (%s)", tc.expect, decision, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
authz.features = tc.features
|
||||
}
|
||||
decision, _, _ := authz.Authorize(context.Background(), tc.attrs)
|
||||
if decision != tc.expect {
|
||||
t.Errorf("expected %v, got %v", tc.expect, decision)
|
||||
}
|
||||
})
|
||||
decision, reason, _ := authz.Authorize(context.Background(), tc.attrs)
|
||||
if decision != tc.expect {
|
||||
t.Errorf("expected %v, got %v (%s)", tc.expect, decision, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizerSharedResources(t *testing.T) {
|
||||
func TestNodeAuthorizerSharedResources(t *testing.T) {
|
||||
g := NewGraph()
|
||||
g.destinationEdgeThreshold = 1
|
||||
identifier := nodeidentifier.NewDefaultNodeIdentifier()
|
||||
@ -564,6 +883,14 @@ type sampleDataOpts struct {
|
||||
nodeResourceCapacitiesPerNode int
|
||||
}
|
||||
|
||||
func mustParseFields(s string) fields.Requirements {
|
||||
selector, err := fields.ParseSelector(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return selector.Requirements()
|
||||
}
|
||||
|
||||
func BenchmarkPopulationAllocation(b *testing.B) {
|
||||
opts := &sampleDataOpts{
|
||||
nodes: 500,
|
||||
|
@ -125,7 +125,7 @@ func NodeRules() []rbacv1.PolicyRule {
|
||||
// TODO: restrict to the bound node as creator in the NodeRestrictions admission plugin
|
||||
rbacv1helpers.NewRule("create", "update", "patch").Groups(legacyGroup).Resources("events").RuleOrDie(),
|
||||
|
||||
// TODO: restrict to pods scheduled on the bound node once field selectors are supported by list/watch authorization
|
||||
// Use the Node authorizer to limit get to pods related to the node, and to limit list/watch to field selectors related to the node.
|
||||
rbacv1helpers.NewRule(Read...).Groups(legacyGroup).Resources("pods").RuleOrDie(),
|
||||
|
||||
// Needed for the node to create/delete mirror pods.
|
||||
|
Loading…
Reference in New Issue
Block a user