Skip to content

Commit 9107df4

Browse files
committed
feat(controller): restrict cross-namespace access to agents/tools
Signed-off-by: Brian Fox <[email protected]>
1 parent 2068674 commit 9107df4

File tree

11 files changed

+1383
-0
lines changed

11 files changed

+1383
-0
lines changed

go/api/v1alpha2/agent_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ type AgentSpec struct {
5959
// and made available to the agent under the `/skills` folder.
6060
// +optional
6161
Skills *SkillForAgent `json:"skills,omitempty"`
62+
63+
// AllowedNamespaces defines which namespaces are allowed to reference this Agent as a tool.
64+
// This follows the Gateway API pattern for cross-namespace route attachments.
65+
// If not specified, only Agents in the same namespace can reference this Agent as a tool.
66+
// This field only applies when this Agent is used as a tool by another Agent.
67+
// See: https://gateway-api.sigs.k8s.io/guides/multiple-ns/#cross-namespace-routing
68+
// +optional
69+
AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"`
6270
}
6371

6472
type SkillForAgent struct {

go/api/v1alpha2/common_types.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,86 @@ import (
2121
"fmt"
2222

2323
"github.com/kagent-dev/kagent/go/internal/utils"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/labels"
2427
"k8s.io/apimachinery/pkg/types"
2528
"sigs.k8s.io/controller-runtime/pkg/client"
2629
)
2730

31+
// FromNamespaces specifies namespace from which references to this resource are allowed.
32+
// This follows the same pattern as Gateway API's cross-namespace route attachment.
33+
// See: https://gateway-api.sigs.k8s.io/guides/multiple-ns/#cross-namespace-routing
34+
// +kubebuilder:validation:Enum=All;Same;Selector
35+
type FromNamespaces string
36+
37+
const (
38+
// NamespacesFromAll allows references from all namespaces.
39+
NamespacesFromAll FromNamespaces = "All"
40+
// NamespacesFromSame only allows references from the same namespace as the target resource (default).
41+
NamespacesFromSame FromNamespaces = "Same"
42+
// NamespacesFromSelector allows references from namespaces matching the selector.
43+
NamespacesFromSelector FromNamespaces = "Selector"
44+
)
45+
46+
// AllowedNamespaces defines which namespaces are allowed to reference this resource.
47+
// This mechanism provides a bidirectional handshake for cross-namespace references,
48+
// following the pattern used by Gateway API for cross-namespace route attachments.
49+
//
50+
// By default (when not specified), only references from the same namespace are allowed.
51+
// +kubebuilder:validation:XValidation:rule="!(self.from == 'Selector' && !has(self.selector))",message="selector must be specified when from is Selector"
52+
type AllowedNamespaces struct {
53+
// From indicates where references to this resource can originate.
54+
// Possible values are:
55+
// * All: References from all namespaces are allowed.
56+
// * Same: Only references from the same namespace are allowed (default).
57+
// * Selector: References from namespaces matching the selector are allowed.
58+
// +kubebuilder:default=Same
59+
// +optional
60+
From FromNamespaces `json:"from,omitempty"`
61+
62+
// Selector is a label selector for namespaces that are allowed to reference this resource.
63+
// Only used when From is set to "Selector".
64+
// +optional
65+
Selector *metav1.LabelSelector `json:"selector,omitempty"`
66+
}
67+
68+
// AllowsNamespace checks if a reference from the given namespace is allowed.
69+
// The targetNamespace is the namespace where the resource being referenced lives.
70+
// The sourceNamespace is the namespace where the referencing resource lives.
71+
func (a *AllowedNamespaces) AllowsNamespace(ctx context.Context, c client.Client, sourceNamespace, targetNamespace string) (bool, error) {
72+
// If AllowedNamespaces is nil, default to same namespace only
73+
if a == nil {
74+
return sourceNamespace == targetNamespace, nil
75+
}
76+
77+
switch a.From {
78+
case NamespacesFromAll:
79+
return true, nil
80+
case NamespacesFromSame, "":
81+
return sourceNamespace == targetNamespace, nil
82+
case NamespacesFromSelector:
83+
if a.Selector == nil {
84+
return false, fmt.Errorf("selector must be specified when from is Selector")
85+
}
86+
87+
// Get the source namespace to check its labels
88+
ns := &corev1.Namespace{}
89+
if err := c.Get(ctx, types.NamespacedName{Name: sourceNamespace}, ns); err != nil {
90+
return false, fmt.Errorf("failed to get namespace %s: %w", sourceNamespace, err)
91+
}
92+
93+
selector, err := metav1.LabelSelectorAsSelector(a.Selector)
94+
if err != nil {
95+
return false, fmt.Errorf("invalid label selector: %w", err)
96+
}
97+
98+
return selector.Matches(labels.Set(ns.Labels)), nil
99+
default:
100+
return false, fmt.Errorf("unknown from value: %s", a.From)
101+
}
102+
}
103+
28104
type ValueSourceType string
29105

30106
const (

0 commit comments

Comments
 (0)