diff --git a/plugin/pkg/admission/noderestriction/admission.go b/plugin/pkg/admission/noderestriction/admission.go index 64353380859..6989a72ff18 100644 --- a/plugin/pkg/admission/noderestriction/admission.go +++ b/plugin/pkg/admission/noderestriction/admission.go @@ -288,6 +288,9 @@ func (p *Plugin) admitPodStatus(nodeName string, a admission.Attributes) error { if !labels.Equals(oldPod.Labels, newPod.Labels) { return admission.NewForbidden(a, fmt.Errorf("node %q cannot update labels through pod status", nodeName)) } + if !resourceClaimStatusesEqual(oldPod.Status.ResourceClaimStatuses, newPod.Status.ResourceClaimStatuses) { + return admission.NewForbidden(a, fmt.Errorf("node %q cannot update resource claim statues", nodeName)) + } return nil default: @@ -295,6 +298,29 @@ func (p *Plugin) admitPodStatus(nodeName string, a admission.Attributes) error { } } +func resourceClaimStatusesEqual(statusA, statusB []api.PodResourceClaimStatus) bool { + if len(statusA) != len(statusB) { + return false + } + // In most cases, status entries only get added once and not modified. + // But this cannot be guaranteed, so for the sake of correctness in all + // cases this code here has to check. + for i := range statusA { + if statusA[i].Name != statusB[i].Name { + return false + } + claimNameA := statusA[i].ResourceClaimName + claimNameB := statusB[i].ResourceClaimName + if (claimNameA == nil) != (claimNameB == nil) { + return false + } + if claimNameA != nil && *claimNameA != *claimNameB { + return false + } + } + return true +} + // admitPodEviction allows to evict a pod if it is assigned to the current node. func (p *Plugin) admitPodEviction(nodeName string, a admission.Attributes) error { switch a.GetOperation() { diff --git a/plugin/pkg/auth/authorizer/node/graph.go b/plugin/pkg/auth/authorizer/node/graph.go index e7e5337a168..85a4b808426 100644 --- a/plugin/pkg/auth/authorizer/node/graph.go +++ b/plugin/pkg/auth/authorizer/node/graph.go @@ -22,6 +22,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/component-helpers/storage/ephemeral" + "k8s.io/dynamic-resource-allocation/resourceclaim" pvutil "k8s.io/kubernetes/pkg/api/v1/persistentvolume" podutil "k8s.io/kubernetes/pkg/api/v1/pod" "k8s.io/kubernetes/third_party/forked/gonum/graph" @@ -117,6 +118,7 @@ const ( podVertexType pvcVertexType pvVertexType + resourceClaimVertexType secretVertexType vaVertexType serviceAccountVertexType @@ -128,6 +130,7 @@ var vertexTypes = map[vertexType]string{ podVertexType: "pod", pvcVertexType: "pvc", pvVertexType: "pv", + resourceClaimVertexType: "resourceclaim", secretVertexType: "secret", vaVertexType: "volumeattachment", serviceAccountVertexType: "serviceAccount", @@ -393,6 +396,20 @@ func (g *Graph) AddPod(pod *corev1.Pod) { g.addEdgeToDestinationIndex_locked(e) } } + + for _, podResourceClaim := range pod.Spec.ResourceClaims { + claimName, _, err := resourceclaim.Name(pod, &podResourceClaim) + // Do we have a valid claim name? If yes, add an edge that grants + // kubelet access to that claim. An error indicates that a claim + // still needs to be created, nil that intentionally no claim + // was created and never will be because it isn't needed. + if err == nil && claimName != nil { + claimVertex := g.getOrCreateVertex_locked(resourceClaimVertexType, pod.Namespace, *claimName) + e := newDestinationEdge(claimVertex, podVertex, nodeVertex) + g.graph.SetEdge(e) + g.addEdgeToDestinationIndex_locked(e) + } + } } func (g *Graph) DeletePod(name, namespace string) { start := time.Now() diff --git a/plugin/pkg/auth/authorizer/node/graph_populator.go b/plugin/pkg/auth/authorizer/node/graph_populator.go index aff1f80c63c..52a808ef788 100644 --- a/plugin/pkg/auth/authorizer/node/graph_populator.go +++ b/plugin/pkg/auth/authorizer/node/graph_populator.go @@ -78,8 +78,9 @@ func (g *graphPopulator) updatePod(oldObj, obj interface{}) { return } if oldPod, ok := oldObj.(*corev1.Pod); ok && oldPod != nil { - if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) { - // Node and uid are unchanged, all object references in the pod spec are immutable + if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) && + resourceClaimStatusesEqual(oldPod.Status.ResourceClaimStatuses, pod.Status.ResourceClaimStatuses) { + // Node and uid are unchanged, all object references in the pod spec are immutable respectively unmodified (claim statuses). klog.V(5).Infof("updatePod %s/%s, node unchanged", pod.Namespace, pod.Name) return } @@ -91,6 +92,29 @@ func (g *graphPopulator) updatePod(oldObj, obj interface{}) { klog.V(5).Infof("updatePod %s/%s for node %s completed in %v", pod.Namespace, pod.Name, pod.Spec.NodeName, time.Since(startTime)) } +func resourceClaimStatusesEqual(statusA, statusB []corev1.PodResourceClaimStatus) bool { + if len(statusA) != len(statusB) { + return false + } + // In most cases, status entries only get added once and not modified. + // But this cannot be guaranteed, so for the sake of correctness in all + // cases this code here has to check. + for i := range statusA { + if statusA[i].Name != statusB[i].Name { + return false + } + claimNameA := statusA[i].ResourceClaimName + claimNameB := statusB[i].ResourceClaimName + if (claimNameA == nil) != (claimNameB == nil) { + return false + } + if claimNameA != nil && *claimNameA != *claimNameB { + return false + } + } + return true +} + func (g *graphPopulator) deletePod(obj interface{}) { if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok { obj = tombstone.Obj diff --git a/plugin/pkg/auth/authorizer/node/node_authorizer.go b/plugin/pkg/auth/authorizer/node/node_authorizer.go index f3a5ab4339c..b03467ffd73 100644 --- a/plugin/pkg/auth/authorizer/node/node_authorizer.go +++ b/plugin/pkg/auth/authorizer/node/node_authorizer.go @@ -30,6 +30,7 @@ import ( "k8s.io/component-base/featuregate" coordapi "k8s.io/kubernetes/pkg/apis/coordination" api "k8s.io/kubernetes/pkg/apis/core" + resourceapi "k8s.io/kubernetes/pkg/apis/resource" storageapi "k8s.io/kubernetes/pkg/apis/storage" "k8s.io/kubernetes/pkg/auth/nodeidentifier" "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" @@ -40,7 +41,7 @@ import ( // NodeAuthorizer authorizes requests from kubelets, with the following logic: // 1. If a request is not from a node (NodeIdentity() returns isNode=false), reject // 2. If a specific node cannot be identified (NodeIdentity() returns nodeName=""), reject -// 3. If a request is for a secret, configmap, persistent volume or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node: +// 3. If a request is for a secret, configmap, persistent volume, resource claim, or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node: // node <- configmap // node <- pod // node <- pod <- secret @@ -48,6 +49,7 @@ import ( // node <- pod <- pvc // node <- pod <- pvc <- pv // node <- pod <- pvc <- pv <- secret +// node <- pod <- ResourceClaim // 4. For other resources, authorize all nodes uniformly using statically defined rules type NodeAuthorizer struct { graph *Graph @@ -72,14 +74,15 @@ func NewAuthorizer(graph *Graph, identifier nodeidentifier.NodeIdentifier, rules } var ( - configMapResource = api.Resource("configmaps") - secretResource = api.Resource("secrets") - pvcResource = api.Resource("persistentvolumeclaims") - pvResource = api.Resource("persistentvolumes") - vaResource = storageapi.Resource("volumeattachments") - svcAcctResource = api.Resource("serviceaccounts") - leaseResource = coordapi.Resource("leases") - csiNodeResource = storageapi.Resource("csinodes") + configMapResource = api.Resource("configmaps") + secretResource = api.Resource("secrets") + pvcResource = api.Resource("persistentvolumeclaims") + pvResource = api.Resource("persistentvolumes") + resourceClaimResource = resourceapi.Resource("resourceclaims") + vaResource = storageapi.Resource("volumeattachments") + svcAcctResource = api.Resource("serviceaccounts") + leaseResource = coordapi.Resource("leases") + csiNodeResource = storageapi.Resource("csinodes") ) func (r *NodeAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { @@ -117,6 +120,8 @@ func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attribu return r.authorizeGet(nodeName, pvcVertexType, attrs) case pvResource: return r.authorizeGet(nodeName, pvVertexType, attrs) + case resourceClaimResource: + return r.authorizeGet(nodeName, resourceClaimVertexType, attrs) case vaResource: return r.authorizeGet(nodeName, vaVertexType, attrs) case svcAcctResource: diff --git a/plugin/pkg/auth/authorizer/node/node_authorizer_test.go b/plugin/pkg/auth/authorizer/node/node_authorizer_test.go index 921a22ca126..b3f02581cee 100644 --- a/plugin/pkg/auth/authorizer/node/node_authorizer_test.go +++ b/plugin/pkg/auth/authorizer/node/node_authorizer_test.go @@ -42,16 +42,19 @@ func TestAuthorizer(t *testing.T) { g := NewGraph() opts := &sampleDataOpts{ - nodes: 2, - namespaces: 2, - podsPerNode: 2, - attachmentsPerNode: 1, - sharedConfigMapsPerPod: 0, - uniqueConfigMapsPerPod: 1, - sharedSecretsPerPod: 1, - uniqueSecretsPerPod: 1, - sharedPVCsPerPod: 0, - uniquePVCsPerPod: 1, + nodes: 2, + namespaces: 2, + podsPerNode: 2, + attachmentsPerNode: 1, + sharedConfigMapsPerPod: 0, + uniqueConfigMapsPerPod: 1, + sharedSecretsPerPod: 1, + uniqueSecretsPerPod: 1, + sharedPVCsPerPod: 0, + uniquePVCsPerPod: 1, + uniqueResourceClaimsPerPod: 1, + uniqueResourceClaimTemplatesPerPod: 1, + uniqueResourceClaimTemplatesWithClaimPerPod: 1, } nodes, pods, pvs, attachments := generate(opts) populate(g, nodes, pods, pvs, attachments) @@ -117,6 +120,16 @@ func TestAuthorizer(t *testing.T) { attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node0", Namespace: "ns0"}, expect: authorizer.DecisionAllow, }, + { + name: "allowed resource claim", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "claim0-pod0-node0-ns0", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, + { + name: "allowed resource claim with template", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "generated-claim-pod0-node0-ns0-0", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, { name: "allowed pv", attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node0-ns0", Namespace: ""}, @@ -142,6 +155,16 @@ func TestAuthorizer(t *testing.T) { attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"}, expect: authorizer.DecisionNoOpinion, }, + { + name: "disallowed resource claim, other node", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "claim0-pod0-node1-ns0", Namespace: "ns0"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed resource claim with template", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "pod0-node1-claimtemplate0", Namespace: "ns0"}, + expect: authorizer.DecisionNoOpinion, + }, { name: "disallowed pv", attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""}, @@ -468,9 +491,12 @@ type sampleDataOpts struct { sharedSecretsPerPod int sharedPVCsPerPod int - uniqueSecretsPerPod int - uniqueConfigMapsPerPod int - uniquePVCsPerPod int + uniqueSecretsPerPod int + uniqueConfigMapsPerPod int + uniquePVCsPerPod int + uniqueResourceClaimsPerPod int + uniqueResourceClaimTemplatesPerPod int + uniqueResourceClaimTemplatesWithClaimPerPod int } func BenchmarkPopulationAllocation(b *testing.B) { @@ -845,6 +871,40 @@ func generatePod(name, namespace, nodeName, svcAccountName string, opts *sampleD PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: pv.Spec.ClaimRef.Name}, }}) } + for i := 0; i < opts.uniqueResourceClaimsPerPod; i++ { + claimName := fmt.Sprintf("claim%d-%s-%s", i, pod.Name, pod.Namespace) + pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{ + Name: fmt.Sprintf("claim%d", i), + Source: corev1.ClaimSource{ + ResourceClaimName: &claimName, + }, + }) + } + for i := 0; i < opts.uniqueResourceClaimTemplatesPerPod; i++ { + claimTemplateName := fmt.Sprintf("claimtemplate%d-%s-%s", i, pod.Name, pod.Namespace) + podClaimName := fmt.Sprintf("claimtemplate%d", i) + pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{ + Name: podClaimName, + Source: corev1.ClaimSource{ + ResourceClaimTemplateName: &claimTemplateName, + }, + }) + } + for i := 0; i < opts.uniqueResourceClaimTemplatesWithClaimPerPod; i++ { + claimTemplateName := fmt.Sprintf("claimtemplate%d-%s-%s", i, pod.Name, pod.Namespace) + podClaimName := fmt.Sprintf("claimtemplate-with-claim%d", i) + claimName := fmt.Sprintf("generated-claim-%s-%s-%d", pod.Name, pod.Namespace, i) + pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{ + Name: podClaimName, + Source: corev1.ClaimSource{ + ResourceClaimTemplateName: &claimTemplateName, + }, + }) + pod.Status.ResourceClaimStatuses = append(pod.Status.ResourceClaimStatuses, corev1.PodResourceClaimStatus{ + Name: podClaimName, + ResourceClaimName: &claimName, + }) + } // Choose shared pvcs randomly from shared pvcs in a namespace. subset = randomSubset(opts.sharedPVCsPerPod, opts.sharedPVCsPerNamespace) for _, i := range subset { diff --git a/test/integration/auth/node_test.go b/test/integration/auth/node_test.go index 7f613de6f72..9021d263181 100644 --- a/test/integration/auth/node_test.go +++ b/test/integration/auth/node_test.go @@ -27,14 +27,18 @@ import ( coordination "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" policy "k8s.io/api/policy/v1" + "k8s.io/api/resource/v1alpha2" storagev1 "k8s.io/api/storage/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + utilfeature "k8s.io/apiserver/pkg/util/feature" clientset "k8s.io/client-go/kubernetes" + featuregatetesting "k8s.io/component-base/featuregate/testing" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/test/integration/framework" "k8s.io/utils/pointer" ) @@ -61,7 +65,10 @@ func TestNodeAuthorizer(t *testing.T) { }, "\n")) tokenFile.Close() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DynamicResourceAllocation, true)() + server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{ + "--runtime-config=api/all=true", "--authorization-mode", "Node,RBAC", "--token-auth-file", tokenFile.Name(), "--enable-admission-plugins", "NodeRestriction", @@ -100,6 +107,13 @@ func TestNodeAuthorizer(t *testing.T) { if _, err := superuserClient.CoreV1().ConfigMaps("ns").Create(context.TODO(), &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "myconfigmap"}}, metav1.CreateOptions{}); err != nil { t.Fatal(err) } + if _, err := superuserClient.ResourceV1alpha2().ResourceClaims("ns").Create(context.TODO(), &v1alpha2.ResourceClaim{ObjectMeta: metav1.ObjectMeta{Name: "mynamedresourceclaim"}, Spec: v1alpha2.ResourceClaimSpec{ResourceClassName: "example.com"}}, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + if _, err := superuserClient.ResourceV1alpha2().ResourceClaims("ns").Create(context.TODO(), &v1alpha2.ResourceClaim{ObjectMeta: metav1.ObjectMeta{Name: "mytemplatizedresourceclaim"}, Spec: v1alpha2.ResourceClaimSpec{ResourceClassName: "example.com"}}, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + pvName := "mypv" if _, err := superuserClientExternal.StorageV1().VolumeAttachments().Create(context.TODO(), &storagev1.VolumeAttachment{ ObjectMeta: metav1.ObjectMeta{Name: "myattachment"}, @@ -169,6 +183,34 @@ func TestNodeAuthorizer(t *testing.T) { return err } } + getResourceClaim := func(client clientset.Interface) func() error { + return func() error { + _, err := client.ResourceV1alpha2().ResourceClaims("ns").Get(context.TODO(), "mynamedresourceclaim", metav1.GetOptions{}) + return err + } + } + getResourceClaimTemplate := func(client clientset.Interface) func() error { + return func() error { + _, err := client.ResourceV1alpha2().ResourceClaims("ns").Get(context.TODO(), "mytemplatizedresourceclaim", metav1.GetOptions{}) + return err + } + } + addResourceClaimTemplateReference := func(client clientset.Interface) func() error { + return func() error { + _, err := client.CoreV1().Pods("ns").Patch(context.TODO(), "node2normalpod", types.MergePatchType, + []byte(`{"status":{"resourceClaimStatuses":[{"name":"templateclaim","resourceClaimName":"mytemplatizedresourceclaim"}]}}`), + metav1.PatchOptions{}, "status") + return err + } + } + removeResourceClaimReference := func(client clientset.Interface) func() error { + return func() error { + _, err := client.CoreV1().Pods("ns").Patch(context.TODO(), "node2normalpod", types.MergePatchType, + []byte(`{"status":{"resourceClaimStatuses":null}}`), + metav1.PatchOptions{}, "status") + return err + } + } createNode2NormalPod := func(client clientset.Interface) func() error { return func() error { @@ -182,6 +224,10 @@ func TestNodeAuthorizer(t *testing.T) { {Name: "cm", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "myconfigmap"}}}}, {Name: "pvc", VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: "mypvc"}}}, }, + ResourceClaims: []corev1.PodResourceClaim{ + {Name: "namedclaim", Source: corev1.ClaimSource{ResourceClaimName: pointer.String("mynamedresourceclaim")}}, + {Name: "templateclaim", Source: corev1.ClaimSource{ResourceClaimTemplateName: pointer.String("myresourceclaimtemplate")}}, + }, }, }, metav1.CreateOptions{}) return err @@ -428,6 +474,8 @@ func TestNodeAuthorizer(t *testing.T) { expectForbidden(t, getConfigMap(nodeanonClient)) expectForbidden(t, getPVC(nodeanonClient)) expectForbidden(t, getPV(nodeanonClient)) + expectForbidden(t, getResourceClaim(nodeanonClient)) + expectForbidden(t, getResourceClaimTemplate(nodeanonClient)) expectForbidden(t, createNode2NormalPod(nodeanonClient)) expectForbidden(t, deleteNode2NormalPod(nodeanonClient)) expectForbidden(t, createNode2MirrorPodEviction(nodeanonClient)) @@ -440,6 +488,8 @@ func TestNodeAuthorizer(t *testing.T) { expectForbidden(t, getConfigMap(node1Client)) expectForbidden(t, getPVC(node1Client)) expectForbidden(t, getPV(node1Client)) + expectForbidden(t, getResourceClaim(node1Client)) + expectForbidden(t, getResourceClaimTemplate(node1Client)) expectForbidden(t, createNode2NormalPod(nodeanonClient)) expectNotFound(t, createNode2MirrorPodEviction(node1Client)) expectForbidden(t, createNode2(node1Client)) @@ -452,6 +502,8 @@ func TestNodeAuthorizer(t *testing.T) { expectForbidden(t, getConfigMap(node2Client)) expectForbidden(t, getPVC(node2Client)) expectForbidden(t, getPV(node2Client)) + expectForbidden(t, getResourceClaim(node2Client)) + expectForbidden(t, getResourceClaimTemplate(node2Client)) expectForbidden(t, createNode2NormalPod(nodeanonClient)) // mirror pod and self node lifecycle is allowed @@ -479,6 +531,8 @@ func TestNodeAuthorizer(t *testing.T) { expectForbidden(t, getConfigMap(nodeanonClient)) expectForbidden(t, getPVC(nodeanonClient)) expectForbidden(t, getPV(nodeanonClient)) + expectForbidden(t, getResourceClaim(nodeanonClient)) + expectForbidden(t, getResourceClaimTemplate(nodeanonClient)) expectForbidden(t, createNode2NormalPod(nodeanonClient)) expectForbidden(t, updateNode2NormalPodStatus(nodeanonClient)) expectForbidden(t, deleteNode2NormalPod(nodeanonClient)) @@ -492,6 +546,8 @@ func TestNodeAuthorizer(t *testing.T) { expectForbidden(t, getConfigMap(node1Client)) expectForbidden(t, getPVC(node1Client)) expectForbidden(t, getPV(node1Client)) + expectForbidden(t, getResourceClaim(node1Client)) + expectForbidden(t, getResourceClaimTemplate(node1Client)) expectForbidden(t, createNode2NormalPod(node1Client)) expectForbidden(t, updateNode2NormalPodStatus(node1Client)) expectForbidden(t, deleteNode2NormalPod(node1Client)) @@ -507,6 +563,26 @@ func TestNodeAuthorizer(t *testing.T) { expectAllowed(t, getPVC(node2Client)) expectAllowed(t, getPV(node2Client)) + // node2 can only get direct claim references + expectAllowed(t, getResourceClaim(node2Client)) + expectForbidden(t, getResourceClaimTemplate(node2Client)) + + // node cannot add a claim reference + expectForbidden(t, addResourceClaimTemplateReference(node2Client)) + // superuser can add a claim reference + expectAllowed(t, addResourceClaimTemplateReference(superuserClient)) + // node can get direct and template claim references + expectAllowed(t, getResourceClaim(node2Client)) + expectAllowed(t, getResourceClaimTemplate(node2Client)) + + // node cannot remove a claim reference + expectForbidden(t, removeResourceClaimReference(node2Client)) + // superuser can remove a claim reference + expectAllowed(t, removeResourceClaimReference(superuserClient)) + // node2 can only get direct claim references + expectAllowed(t, getResourceClaim(node2Client)) + expectForbidden(t, getResourceClaimTemplate(node2Client)) + expectForbidden(t, createNode2NormalPod(node2Client)) expectAllowed(t, updateNode2NormalPodStatus(node2Client)) expectAllowed(t, deleteNode2NormalPod(node2Client))