@@ -19,6 +19,9 @@ package v1
1919import (
2020 "context"
2121 "fmt"
22+ "maps"
23+ "slices"
24+ "strings"
2225
2326 corev1 "k8s.io/api/core/v1"
2427 apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -35,6 +38,46 @@ import (
3538 "github.com/kubewarden/kubewarden-controller/internal/constants"
3639)
3740
41+ // capabilityNode is a node in the host-capability path tree.
42+ // Leaf nodes (complete, addressable operations) have a nil value.
43+ // Intermediate nodes carry a non-nil map of named children.
44+ type capabilityNode map [string ]capabilityNode
45+
46+ // capabilityTree is the authoritative tree of all recognised host capability
47+ // paths. It mirrors the namespaces and operations handled by the policy-server
48+ // callback (crates/policy-server/src/evaluation/callback.rs).
49+ //
50+ //nolint:gochecknoglobals // effectively a constant, not used anywhere else
51+ var capabilityTree = capabilityNode {
52+ "oci" : {
53+ "v1" : {
54+ "verify" : nil ,
55+ "manifest_digest" : nil ,
56+ "oci_manifest" : nil ,
57+ "oci_manifest_config" : nil ,
58+ },
59+ "v2" : {
60+ "verify" : nil ,
61+ },
62+ },
63+ "net" : {
64+ "v1" : {
65+ "dns_lookup_host" : nil ,
66+ },
67+ },
68+ "crypto" : {
69+ "v1" : {
70+ "is_certificate_trusted" : nil ,
71+ },
72+ },
73+ "kubernetes" : {
74+ "list_resources_by_namespace" : nil ,
75+ "list_resources_all" : nil ,
76+ "get_resource" : nil ,
77+ "can_i" : nil ,
78+ },
79+ }
80+
3881// SetupWebhookWithManager registers the PolicyServer webhook with the controller manager.
3982func (ps * PolicyServer ) SetupWebhookWithManager (mgr ctrl.Manager , deploymentsNamespace string ) error {
4083 logger := mgr .GetLogger ().WithName ("policyserver-webhook" )
@@ -131,6 +174,7 @@ func (v *policyServerValidator) validate(ctx context.Context, policyServer *Poli
131174 }
132175
133176 allErrs = append (allErrs , validateLimitsAndRequests (policyServer .Spec .Limits , policyServer .Spec .Requests )... )
177+ allErrs = append (allErrs , validateNamespacedPoliciesCapabilities (policyServer .Spec .NamespacedPoliciesCapabilities )... )
134178
135179 if len (allErrs ) == 0 {
136180 return nil
@@ -208,3 +252,78 @@ func validateLimitsAndRequests(limits, requests corev1.ResourceList) field.Error
208252
209253 return allErrs
210254}
255+
256+ // validateNamespacedPoliciesCapabilities validates each capability pattern
257+ // against the authoritative capability tree.
258+ //
259+ // Valid formats:
260+ // - "*" (allow all capabilities)
261+ // - "category/*" (e.g. "oci/*", "kubernetes/*")
262+ // - "category/sub/*" (e.g. "oci/v1/*")
263+ // - full path (e.g. "oci/v1/verify", "kubernetes/can_i")
264+ //
265+ // Every segment is validated against the tree, so unknown categories,
266+ // unknown versions, and unknown operations are all rejected with an error
267+ // listing the valid options at that level.
268+ func validateNamespacedPoliciesCapabilities (capabilities []string ) field.ErrorList {
269+ var allErrs field.ErrorList
270+ fieldPath := field .NewPath ("spec" ).Child ("namespacedPoliciesCapabilities" )
271+
272+ for i , pattern := range capabilities {
273+ if err := validateSingleCapability (pattern , fieldPath .Index (i )); err != nil {
274+ allErrs = append (allErrs , err )
275+ }
276+ }
277+
278+ return allErrs
279+ }
280+
281+ // validateSingleCapability validates one capability pattern against the capability tree.
282+ func validateSingleCapability (pattern string , path * field.Path ) * field.Error {
283+ if pattern == "" {
284+ return field .Invalid (path , pattern , "capability must not be empty" )
285+ }
286+ if pattern == "*" {
287+ return nil
288+ }
289+
290+ parts := strings .Split (pattern , "/" )
291+ node := capabilityTree
292+
293+ for i , part := range parts {
294+ // Wildcard handling: "*" is only valid as the final segment.
295+ if strings .Contains (part , "*" ) {
296+ if part != "*" || i != len (parts )- 1 {
297+ return field .Invalid (path , pattern ,
298+ "wildcard \" *\" is only allowed as the last path segment (e.g. \" oci/*\" or \" oci/v1/*\" )" )
299+ }
300+ // Valid wildcard termination; parent node is already confirmed.
301+ return nil
302+ }
303+
304+ child , found := node [part ]
305+ if ! found {
306+ return field .Invalid (path , pattern ,
307+ fmt .Sprintf ("unknown segment %q, valid options at this level are: %s" ,
308+ part , strings .Join (slices .Sorted (maps .Keys (node )), ", " )))
309+ }
310+
311+ if child == nil {
312+ // Leaf reached, path must end here.
313+ if i != len (parts )- 1 {
314+ return field .Invalid (path , pattern ,
315+ fmt .Sprintf ("%q is a complete capability path and cannot have further segments" ,
316+ strings .Join (parts [:i + 1 ], "/" )))
317+ }
318+ return nil
319+ }
320+
321+ node = child
322+ }
323+
324+ // Consumed all parts but stopped at an intermediate node: the path is
325+ // incomplete. Guide the user toward the wildcard form.
326+ return field .Invalid (path , pattern ,
327+ fmt .Sprintf ("%q is not a complete capability path; use %q to allow all capabilities under it" ,
328+ pattern , pattern + "/*" ))
329+ }
0 commit comments