Skip to content

Commit c927057

Browse files
committed
feat: add namespace support to Rego driver via input.namespace
- Add Namespace field to ReviewCfg for passing namespace data to drivers - Add reviews.Namespace() option function for callers to pass namespace - Update Rego driver to accept namespace in toParsedInput and pass to Query - Update hookModuleRego to include input.namespace for Rego policy access - Add TemplateCheckNamespace for testing namespace-based policies - Add WantEnvironment constraint argument for namespace tests - Add TestClient_Review_Namespace e2e test for Rego namespace support This enables Rego policies to access namespace metadata via input.namespace for namespace-scoped policy decisions.
1 parent a9c75a1 commit c927057

File tree

9 files changed

+199
-7
lines changed

9 files changed

+199
-7
lines changed

constraint/pkg/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import (
1818
"github.com/open-policy-agent/frameworks/constraint/pkg/handler"
1919
"github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation"
2020
"github.com/open-policy-agent/frameworks/constraint/pkg/types"
21+
admissionv1 "k8s.io/api/admission/v1"
2122
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
2223
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23-
admissionv1 "k8s.io/api/admission/v1"
2424
)
2525

2626
const statusField = "status"

constraint/pkg/client/clienttest/cts/constraints.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ func WantData(data string) ConstraintArg {
127127
}
128128
}
129129

130+
// WantEnvironment sets the expected namespace environment label.
131+
// Only meaningful for CheckNamespace constraints.
132+
func WantEnvironment(env string) ConstraintArg {
133+
return func(u *unstructured.Unstructured) error {
134+
return unstructured.SetNestedField(u.Object, env, "spec", "parameters", "wantEnvironment")
135+
}
136+
}
137+
130138
// EnforcementAction sets the action to be taken if the Constraint is violated.
131139
func EnforcementAction(action string) ConstraintArg {
132140
return func(u *unstructured.Unstructured) error {

constraint/pkg/client/clienttest/templates.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,60 @@ func TemplateFuture() *templates.ConstraintTemplate {
372372

373373
return ct
374374
}
375+
376+
const KindCheckNamespace = "CheckNamespace"
377+
378+
// moduleCheckNamespace defines a Rego package which checks the namespace object
379+
// passed via input.namespace. This tests that namespace data is available to
380+
// Rego policies for namespace-based policy decisions.
381+
const moduleCheckNamespace = `
382+
package foo
383+
384+
violation[{"msg": msg}] {
385+
# Check if namespace is provided and has the expected label
386+
ns := input.namespace
387+
not ns.metadata.labels.environment
388+
msg := "namespace is missing environment label"
389+
}
390+
391+
violation[{"msg": msg}] {
392+
# Check if namespace has specific label value
393+
ns := input.namespace
394+
ns.metadata.labels.environment
395+
ns.metadata.labels.environment != input.parameters.wantEnvironment
396+
msg := sprintf("namespace has environment %v but want %v", [ns.metadata.labels.environment, input.parameters.wantEnvironment])
397+
}
398+
`
399+
400+
// TemplateCheckNamespace returns a ConstraintTemplate that validates namespace
401+
// labels via input.namespace. This tests the Rego driver's namespace support.
402+
func TemplateCheckNamespace() *templates.ConstraintTemplate {
403+
ct := &templates.ConstraintTemplate{}
404+
405+
ct.SetName("checknamespace")
406+
ct.Spec.CRD.Spec.Names.Kind = KindCheckNamespace
407+
ct.Spec.CRD.Spec.Validation = &templates.Validation{
408+
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
409+
Type: "object",
410+
Properties: map[string]apiextensions.JSONSchemaProps{
411+
"wantEnvironment": {Type: "string"},
412+
},
413+
},
414+
}
415+
416+
ct.Spec.Targets = []templates.Target{{
417+
Target: handlertest.TargetName,
418+
Code: []templates.Code{
419+
{
420+
Engine: schema.Name,
421+
Source: &templates.Anything{
422+
Value: (&schema.Source{
423+
Rego: moduleCheckNamespace,
424+
}).ToUnstructured(),
425+
},
426+
},
427+
},
428+
}}
429+
430+
return ct
431+
}

constraint/pkg/client/drivers/rego/driver.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru
286286
}
287287

288288
// Parse input into an ast.Value to avoid round-tripping through JSON when
289-
// possible.
290-
parsedInput, err := toParsedInput(target, kindConstraints, reviewMap)
289+
// possible. Include namespace if provided for namespace-based policies.
290+
parsedInput, err := toParsedInput(target, kindConstraints, reviewMap, cfg.Namespace)
291291
if err != nil {
292292
return nil, err
293293
}
@@ -502,7 +502,7 @@ func toConstraintsByKind(constraints []*unstructured.Unstructured) map[string][]
502502
return constraintsByKind
503503
}
504504

505-
func toParsedInput(target string, constraints []*unstructured.Unstructured, review map[string]interface{}) (ast.Value, error) {
505+
func toParsedInput(target string, constraints []*unstructured.Unstructured, review map[string]interface{}, namespace map[string]interface{}) (ast.Value, error) {
506506
// Store constraint keys in a format InterfaceToValue does not need to
507507
// round-trip through JSON.
508508
constraintKeys := toKeySlice(constraints)
@@ -513,6 +513,12 @@ func toParsedInput(target string, constraints []*unstructured.Unstructured, revi
513513
"review": review,
514514
}
515515

516+
// Add namespace object if available (for namespaced resources).
517+
// This enables policies to access namespace labels and metadata via input.namespace.
518+
if namespace != nil {
519+
input["namespace"] = namespace
520+
}
521+
516522
// Parse input into an ast.Value to avoid round-tripping through JSON when
517523
// possible.
518524
return ast.InterfaceToValue(input)

constraint/pkg/client/drivers/rego/rego.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ violation[response] {
2626
key := input.constraints[_]
2727
# Construct the input object from the Constraint and temporary object in storage.
2828
# Silently exits if the Constraint no longer exists.
29+
# Include namespace if available for namespace-based policies.
2930
inp := {
3031
"review": input.review,
3132
"parameters": data.constraints[key.kind][key.name],
33+
"namespace": object.get(input, "namespace", null),
3234
}
3335
# Run the Template with Constraint.
3436
data.template.violation[r] with input as inp

constraint/pkg/client/e2e_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,3 +1130,108 @@ func TestE2E_Client_GetDescriptionForStat(t *testing.T) {
11301130
}
11311131
}
11321132
}
1133+
1134+
// TestClient_Review_Namespace tests that namespace data is properly passed
1135+
// to the Rego driver via input.namespace for namespace-based policy decisions.
1136+
func TestClient_Review_Namespace(t *testing.T) {
1137+
tests := []struct {
1138+
name string
1139+
namespace map[string]interface{}
1140+
wantEnv string
1141+
wantResults int
1142+
wantMsg string
1143+
}{
1144+
{
1145+
name: "no namespace provided - policy skips check",
1146+
namespace: nil,
1147+
wantEnv: "production",
1148+
wantResults: 1, // Violation because input.namespace is nil, missing environment label check triggers
1149+
wantMsg: "namespace is missing environment label",
1150+
},
1151+
{
1152+
name: "namespace with matching environment label",
1153+
namespace: map[string]interface{}{
1154+
"metadata": map[string]interface{}{
1155+
"name": "test-ns",
1156+
"labels": map[string]interface{}{
1157+
"environment": "production",
1158+
},
1159+
},
1160+
},
1161+
wantEnv: "production",
1162+
wantResults: 0, // No violation - environment matches
1163+
},
1164+
{
1165+
name: "namespace with wrong environment label",
1166+
namespace: map[string]interface{}{
1167+
"metadata": map[string]interface{}{
1168+
"name": "test-ns",
1169+
"labels": map[string]interface{}{
1170+
"environment": "staging",
1171+
},
1172+
},
1173+
},
1174+
wantEnv: "production",
1175+
wantResults: 1,
1176+
wantMsg: "namespace has environment staging but want production",
1177+
},
1178+
{
1179+
name: "namespace missing environment label",
1180+
namespace: map[string]interface{}{
1181+
"metadata": map[string]interface{}{
1182+
"name": "test-ns",
1183+
"labels": map[string]interface{}{
1184+
"team": "platform",
1185+
},
1186+
},
1187+
},
1188+
wantEnv: "production",
1189+
wantResults: 1,
1190+
wantMsg: "namespace is missing environment label",
1191+
},
1192+
}
1193+
1194+
for _, tt := range tests {
1195+
t.Run(tt.name, func(t *testing.T) {
1196+
ctx := context.Background()
1197+
1198+
c := clienttest.New(t)
1199+
1200+
ct := clienttest.TemplateCheckNamespace()
1201+
_, err := c.AddTemplate(ctx, ct)
1202+
if err != nil {
1203+
t.Fatal(err)
1204+
}
1205+
1206+
constraint := cts.MakeConstraint(t, clienttest.KindCheckNamespace, "constraint", cts.WantEnvironment(tt.wantEnv))
1207+
_, err = c.AddConstraint(ctx, constraint)
1208+
if err != nil {
1209+
t.Fatal(err)
1210+
}
1211+
1212+
review := handlertest.NewReview("test-ns", "test-obj", "test-data")
1213+
1214+
// Pass namespace via reviews.Namespace option
1215+
var opts []reviews.ReviewOpt
1216+
if tt.namespace != nil {
1217+
opts = append(opts, reviews.Namespace(tt.namespace))
1218+
}
1219+
1220+
responses, err := c.Review(ctx, review, opts...)
1221+
if err != nil {
1222+
t.Fatal(err)
1223+
}
1224+
1225+
results := responses.Results()
1226+
if len(results) != tt.wantResults {
1227+
t.Errorf("got %d results, want %d. Results: %v", len(results), tt.wantResults, results)
1228+
}
1229+
1230+
if tt.wantResults > 0 && len(results) > 0 {
1231+
if results[0].Msg != tt.wantMsg {
1232+
t.Errorf("got message %q, want %q", results[0].Msg, tt.wantMsg)
1233+
}
1234+
}
1235+
})
1236+
}
1237+
}

constraint/pkg/client/reviews/review_opts.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package reviews
22

3+
// ReviewCfg contains configuration options for a single review query.
34
type ReviewCfg struct {
45
TracingEnabled bool
56
StatsEnabled bool
67
EnforcementPoint string
8+
// Namespace is the namespace object for the resource being reviewed.
9+
// For namespaced resources, this contains the full namespace object
10+
// including metadata and labels. For cluster-scoped resources, this is nil.
11+
Namespace map[string]interface{}
712
}
813

914
// ReviewOpt specifies optional arguments for Query driver calls.
@@ -32,3 +37,12 @@ func EnforcementPoint(ep string) ReviewOpt {
3237
cfg.EnforcementPoint = ep
3338
}
3439
}
40+
41+
// Namespace sets the namespace object for the review.
42+
// This makes the namespace available to policy templates as input.namespace (Rego)
43+
// or namespaceObject (CEL), enabling policies to access namespace labels and metadata.
44+
func Namespace(ns map[string]interface{}) ReviewOpt {
45+
return func(cfg *ReviewCfg) {
46+
cfg.Namespace = ns
47+
}
48+
}

constraint/pkg/client/template_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func makeMatchers(targets []handler.TargetHandler, constraint *unstructured.Unst
191191
return result, nil
192192
}
193193

194-
// MatchesOperation checks if the given operation type matches any of the template's target operations
194+
// MatchesOperation checks if the given operation type matches any of the template's target operations.
195195
func (e *templateClient) MatchesOperation(operation string) bool {
196196
if len(e.template.Spec.Targets) != 1 {
197197
// for backward compatibility, matching all templates by default

constraint/pkg/client/template_client_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func TestTemplateClient_MatchesOperation_BackwardCompatibility(t *testing.T) {
253253
}
254254
}
255255

256-
// Benchmark test to ensure the function is performant
256+
// Benchmark test to ensure the function is performant.
257257
func BenchmarkTemplateClient_MatchesOperation(b *testing.B) {
258258
tc := &templateClient{
259259
template: &templates.ConstraintTemplate{
@@ -277,4 +277,4 @@ func BenchmarkTemplateClient_MatchesOperation(b *testing.B) {
277277
for i := 0; i < b.N; i++ {
278278
tc.MatchesOperation("UPDATE")
279279
}
280-
}
280+
}

0 commit comments

Comments
 (0)