Proof of Concept
// PoC: Demonstrates the authorization edge-case and how the fix alters access decisions
// This is a conceptual reproduction based on the unit test in the commit.
// It assumes an in-memory graph and an authorizer similar to Kubernetes' internal tests.
package main
import (
"context"
"fmt"
"k8s.io/kubernetes/pkg/auth/authorizer"
authznode "k8s.io/kubernetes/plugin/pkg/auth/authorizer/node"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/node"
"k8s.io/kubernetes/pkg/auth/authorizer/interfaces"
user "k8s.io/pod-security-admission/pkg/user"
)
// This is a conceptual PoC sketch mirroring the test logic in the patch.
func main() {
// Setup: in-memory graph and authorizer
g := NewGraph()
identifier := nodeidentifier.NewDefaultNodeIdentifier()
authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
node1 := &user.DefaultInfo{Name: "system:node:node1", Groups: []string{"system:nodes"}}
// Pod bound to node1, with ExtendedResourceClaimStatus but no standard ResourceClaims
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "ns1",
UID: "pod1uid",
},
Spec: corev1.PodSpec{ NodeName: "node1" },
Status: corev1.PodStatus{
ExtendedResourceClaimStatus: &corev1.PodExtendedResourceClaimStatus{
ResourceClaimName: "extended-claim-0",
RequestMappings: []corev1.ContainerExtendedResourceRequest{{ ContainerName: "container0", ResourceName: "example.com/gpu", RequestName: "request" }},
},
},
}
p := &graphPopulator{}
p.graph = g
p.addPod(pod)
// Before change: querying access to extended-claim-1 should yield NoOpinion
ctx := context.Background()
decision, _, err := authz.Authorize(ctx, authorizer.AttributesRecord{
User: node1,
ResourceRequest: true,
Verb: "get",
Resource: "resourceclaims",
APIGroup: "resource.k8s.io",
Namespace: "ns1",
Name: "extended-claim-1",
})
if err != nil { panic(err) }
fmt.Println("pre-change decision:", decision.String()) // expect NoOpinion
// Now simulate scheduler updating ExtendedResourceClaimStatus to a new claim
updatedPod := pod.DeepCopy()
updatedPod.Status.ExtendedResourceClaimStatus.ResourceClaimName = "extended-claim-1"
p.updatePod(pod, updatedPod)
// After change: node1 should gain access to extended-claim-1
decision, _, err = authz.Authorize(ctx, authorizer.AttributesRecord{
User: node1,
ResourceRequest: true,
Verb: "get",
Resource: "resourceclaims",
APIGroup: "resource.k8s.io",
Namespace: "ns1",
Name: "extended-claim-1",
})
if err != nil { panic(err) }
fmt.Println("post-change decision:", decision.String()) // expect Allow
// A different node should still have no access
node2 := &user.DefaultInfo{Name: "system:node:node2", Groups: []string{"system:nodes"}}
decision, _, err = authz.Authorize(ctx, authorizer.AttributesRecord{
User: node2,
ResourceRequest: true,
Verb: "get",
Resource: "resourceclaims",
APIGroup: "resource.k8s.io",
Namespace: "ns1",
Name: "extended-claim-1",
})
if err != nil { panic(err) }
fmt.Println("node2 decision:", decision.String()) // expect NoOpinion
}
Code Diff
diff --git a/plugin/pkg/auth/authorizer/node/graph_populator.go b/plugin/pkg/auth/authorizer/node/graph_populator.go
index d10c229d3dc7f..3617588f804a8 100644
--- a/plugin/pkg/auth/authorizer/node/graph_populator.go
+++ b/plugin/pkg/auth/authorizer/node/graph_populator.go
@@ -111,7 +111,8 @@ func (g *graphPopulator) updatePod(oldObj, obj interface{}) {
hasNewEphemeralContainers := len(pod.Spec.EphemeralContainers) > len(oldPod.Spec.EphemeralContainers)
if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) &&
!hasNewEphemeralContainers &&
- resourceclaim.PodStatusEqual(oldPod.Status.ResourceClaimStatuses, pod.Status.ResourceClaimStatuses) {
+ resourceclaim.PodStatusEqual(oldPod.Status.ResourceClaimStatuses, pod.Status.ResourceClaimStatuses) &&
+ resourceclaim.PodExtendedStatusEqual(oldPod.Status.ExtendedResourceClaimStatus, pod.Status.ExtendedResourceClaimStatus) {
// 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
diff --git a/plugin/pkg/auth/authorizer/node/node_authorizer_test.go b/plugin/pkg/auth/authorizer/node/node_authorizer_test.go
index 3f72477835103..1dbe4cd0974df 100644
--- a/plugin/pkg/auth/authorizer/node/node_authorizer_test.go
+++ b/plugin/pkg/auth/authorizer/node/node_authorizer_test.go
@@ -1058,6 +1058,120 @@ func TestNodeAuthorizerAddEphemeralContainers(t *testing.T) {
}
}
+// TestNodeAuthorizerUpdateExtendedResourceClaim checks that a node gains
+// authorization to read its pod's synthesized ResourceClaim once the scheduler
+// writes ExtendedResourceClaimStatus into the pod — even when the pod carries
+// no standard Spec.ResourceClaims entries.
+//
+// Background: the graph populator's updatePod has a fast-path that skips
+// AddPod when the pod's node assignment, UID, ephemeral containers, and
+// ResourceClaimStatuses are all unchanged. For pods that use the
+// DRAExtendedResource path (e.g. a plain nvidia.com/gpu request),
+// ResourceClaimStatuses is nil both before and after the scheduler writes
+// the synthesized claim name, and ExtendedResourceClaimStatus may change
+// under rare condition after a pod is bound to a node, so the fast-path
+// would fire prematurely and the claim→pod→node edge would never be added
+// to the authorization graph.
+func TestNodeAuthorizerUpdateExtendedResourceClaim(t *testing.T) {
+ g := NewGraph()
+ identifier := nodeidentifier.NewDefaultNodeIdentifier()
+ authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
+
+ node1 := &user.DefaultInfo{Name: "system:node:node1", Groups: []string{"system:nodes"}}
+
+ // The pod has been bound to node1, but the scheduler has written
+ // extended-claim-0, not extended-claim-1, to ExtendedResourceClaimStatus.
+ // There are no standard Spec.ResourceClaims, so ResourceClaimStatuses
+ // stays nil throughout.
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pod1",
+ Namespace: "ns1",
+ UID: "pod1uid",
+ },
+ Spec: corev1.PodSpec{
+ NodeName: "node1",
+ },
+ Status: corev1.PodStatus{
+ ExtendedResourceClaimStatus: &corev1.PodExtendedResourceClaimStatus{
+ ResourceClaimName: "extended-claim-0",
+ RequestMappings: []corev1.ContainerExtendedResourceRequest{
+ {
+ ContainerName: "container0",
+ ResourceName: "example.com/gpu",
+ RequestName: "request",
+ },
+ },
+ },
+ },
+ }
+
+ p := &graphPopulator{}
+ p.graph = g
+ p.addPod(pod)
+
+ // Before the scheduler swaps the synthesized claim name, extended-claim-1
+ // is not in the graph and node1 should have no opinion on it.
+ decision, _, err := authz.Authorize(context.Background(), authorizer.AttributesRecord{
+ User: node1,
+ ResourceRequest: true,
+ Verb: "get",
+ Resource: "resourceclaims",
+ APIGroup: "resource.k8s.io",
+ Namespace: "ns1",
+ Name: "extended-claim-1",
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if decision != authorizer.DecisionNoOpinion {
+ t.Errorf("before ExtendedResourceClaimStatus is updated to extended-claim-1: want NoOpinion, got %v", decision)
+ }
+
+ // The scheduler swaps the synthesized ResourceClaim from extended-claim-0
+ // to extended-claim-1. updatePod must recognize that ExtendedResourceClaimStatus
+ // changed and rebuild the graph edge rather than taking the fast-path early
+ // exit.
+ updatedPod := pod.DeepCopy()
+ updatedPod.Status.ExtendedResourceClaimStatus.ResourceClaimName = "extended-claim-1"
+ p.updatePod(pod, updatedPod)
+
+ // The node that hosts the pod should now be permitted to read the claim.
+ decision, _, err = authz.Authorize(context.Background(), authorizer.AttributesRecord{
+ User: node1,
+ ResourceRequest: true,
+ Verb: "get",
+ Resource: "resourceclaims",
+ APIGroup: "resource.k8s.io",
+ Namespace: "ns1",
+ Name: "extended-claim-1",
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if decision != authorizer.DecisionAllow {
+ t.Errorf("after ExtendedResourceClaimStatus is updated to extended-claim-1: want Allow, got %v", decision)
+ }
+
+ // A node that does not host the pod must not gain access to the claim.
+ node2 := &user.DefaultInfo{Name: "system:node:node2", Groups: []string{"system:nodes"}}
+ decision, _, err = authz.Authorize(context.Background(), authorizer.AttributesRecord{
+ User: node2,
+ ResourceRequest: true,
+ Verb: "get",
+ Resource: "resourceclaims",
+ APIGroup: "resource.k8s.io",
+ Namespace: "ns1",
+ Name: "extended-claim-1",
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if decision != authorizer.DecisionNoOpinion {
+ t.Errorf("node2 should not access a claim belonging to node1's pod: want NoOpinion, got %v", decision)
+ }
+}
+
type sampleDataOpts struct {
nodes int
namespaces int