From d47684c0d90768589e8aea8681e8a92825db426b Mon Sep 17 00:00:00 2001 From: Mariam Fahmy Date: Tue, 23 Jan 2024 13:47:38 +0200 Subject: [PATCH] feat: support validatingadmissionpolicybindings in CLI apply command (#9468) * feat: support validatingadmissionpolicybindings in CLI apply command Signed-off-by: Mariam Fahmy * fix linter issue Signed-off-by: Mariam Fahmy --------- Signed-off-by: Mariam Fahmy --- .../kubectl-kyverno/commands/apply/command.go | 48 +++-- .../commands/fix/policy/options.go | 12 +- .../commands/oci/pull/options.go | 2 +- .../commands/oci/push/options.go | 2 +- cmd/cli/kubectl-kyverno/commands/test/test.go | 2 +- cmd/cli/kubectl-kyverno/policy/load.go | 99 +++++---- cmd/cli/kubectl-kyverno/policy/load_test.go | 4 +- .../kubectl-kyverno/policy/variables_test.go | 2 +- .../processor/policy_processor_test.go | 2 +- .../processor/vap_processor.go | 11 +- cmd/cli/kubectl-kyverno/report/report_test.go | 8 +- pkg/autogen/autogen_test.go | 6 +- .../report/background/controller.go | 2 +- pkg/controllers/report/utils/scanner.go | 10 +- .../policycheck_test.go | 2 +- pkg/utils/yaml/loadpolicy.go | 49 +++-- pkg/utils/yaml/loadpolicy_test.go | 87 ++++++-- pkg/validatingadmissionpolicy/api.go | 68 +++++++ pkg/validatingadmissionpolicy/utils.go | 54 +++++ .../validatingadmissionpolicy.go | 191 ++++++++++++++---- .../validatingadmissionpolicy_test.go | 2 +- pkg/validation/policy/allowed_vars_test.go | 20 +- 22 files changed, 516 insertions(+), 167 deletions(-) create mode 100644 pkg/validatingadmissionpolicy/api.go create mode 100644 pkg/validatingadmissionpolicy/utils.go diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command.go b/cmd/cli/kubectl-kyverno/commands/apply/command.go index 43f6ac9ab0..c72038a6a0 100644 --- a/cmd/cli/kubectl-kyverno/commands/apply/command.go +++ b/cmd/cli/kubectl-kyverno/commands/apply/command.go @@ -148,11 +148,11 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } - rc, resources1, skipInvalidPolicies, responses1, policies, validatingAdmissionPolicies, err := c.loadPolicies(skipInvalidPolicies) + rc, resources1, skipInvalidPolicies, responses1, policies, vaps, vapBindings, err := c.loadPolicies(skipInvalidPolicies) if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } - resources, err := c.loadResources(out, policies, validatingAdmissionPolicies, dClient) + resources, err := c.loadResources(out, policies, vaps, dClient) if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } @@ -161,7 +161,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul for _, policy := range policies { policyRulesCount += len(autogen.ComputeRules(policy)) } - policyRulesCount += len(validatingAdmissionPolicies) + policyRulesCount += len(vaps) fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources)) } @@ -179,7 +179,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } - responses2, err := c.applyValidatingAdmissionPolicytoResource(variables, validatingAdmissionPolicies, resources1, rc, dClient, &skipInvalidPolicies) + responses2, err := c.applyValidatingAdmissionPolicytoResource(vaps, vapBindings, resources1, rc, dClient) if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } @@ -198,20 +198,21 @@ func (c *ApplyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies SkippedIn } func (c *ApplyCommandConfig) applyValidatingAdmissionPolicytoResource( - variables *variables.Variables, - validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, + vaps []v1alpha1.ValidatingAdmissionPolicy, + vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding, resources []*unstructured.Unstructured, rc *processor.ResultCounts, dClient dclient.Interface, - skipInvalidPolicies *SkippedInvalidPolicies, ) ([]engineapi.EngineResponse, error) { var responses []engineapi.EngineResponse for _, resource := range resources { processor := processor.ValidatingAdmissionPolicyProcessor{ - Policies: validatingAdmissionPolicies, + Policies: vaps, + Bindings: vapBindings, Resource: resource, PolicyReport: c.PolicyReport, Rc: rc, + Client: dClient, } ers, err := processor.ApplyPolicyOnResource() if err != nil { @@ -283,18 +284,19 @@ func (c *ApplyCommandConfig) applyPolicytoResource( return &rc, resources, responses, nil } -func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) { - resources, err := common.GetResourceAccordingToResourcePath(out, nil, c.ResourcePaths, c.Cluster, policies, validatingAdmissionPolicies, dClient, c.Namespace, c.PolicyReport, "") +func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []v1alpha1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) { + resources, err := common.GetResourceAccordingToResourcePath(out, nil, c.ResourcePaths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "") if err != nil { return resources, fmt.Errorf("failed to load resources (%w)", err) } return resources, nil } -func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, []kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, []kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { // load policies var policies []kyvernov1.PolicyInterface - var validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy + var vaps []v1alpha1.ValidatingAdmissionPolicy + var vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding for _, path := range c.PolicyPaths { isGit := source.IsGit(path) @@ -302,13 +304,13 @@ func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPoli if isGit { gitSourceURL, err := url.Parse(path) if err != nil { - return nil, nil, skipInvalidPolicies, nil, nil, nil, fmt.Errorf("failed to load policies (%w)", err) + return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to load policies (%w)", err) } pathElems := strings.Split(gitSourceURL.Path[1:], "/") if len(pathElems) <= 1 { err := fmt.Errorf("invalid URL path %s - expected https:///:owner/:repository/:branch (without --git-branch flag) OR https:///:owner/:repository/:directory (with --git-branch flag)", gitSourceURL.Path) - return nil, nil, skipInvalidPolicies, nil, nil, nil, fmt.Errorf("failed to parse URL (%w)", err) + return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to parse URL (%w)", err) } gitSourceURL.Path = strings.Join([]string{pathElems[0], pathElems[1]}, "/") repoURL := gitSourceURL.String() @@ -317,31 +319,33 @@ func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPoli fs := memfs.New() if _, err := gitutils.Clone(repoURL, fs, c.GitBranch); err != nil { log.Log.V(3).Info(fmt.Sprintf("failed to clone repository %v as it is not valid", repoURL), "error", err) - return nil, nil, skipInvalidPolicies, nil, nil, nil, fmt.Errorf("failed to clone repository (%w)", err) + return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to clone repository (%w)", err) } policyYamls, err := gitutils.ListYamls(fs, gitPathToYamls) if err != nil { - return nil, nil, skipInvalidPolicies, nil, nil, nil, fmt.Errorf("failed to list YAMLs in repository (%w)", err) + return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to list YAMLs in repository (%w)", err) } for _, policyYaml := range policyYamls { - policiesFromFile, admissionPoliciesFromFile, err := policy.Load(fs, "", policyYaml) + policiesFromFile, vapsFromFile, vapBindingsFromFile, err := policy.Load(fs, "", policyYaml) if err != nil { continue } policies = append(policies, policiesFromFile...) - validatingAdmissionPolicies = append(validatingAdmissionPolicies, admissionPoliciesFromFile...) + vaps = append(vaps, vapsFromFile...) + vapBindings = append(vapBindings, vapBindingsFromFile...) } } else { - policiesFromFile, admissionPoliciesFromFile, err := policy.Load(nil, "", path) + policiesFromFile, vapsFromFile, vapBindingsFromFile, err := policy.Load(nil, "", path) if err != nil { - return nil, nil, skipInvalidPolicies, nil, nil, nil, fmt.Errorf("failed to load policies (%w)", err) + return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to load policies (%w)", err) } policies = append(policies, policiesFromFile...) - validatingAdmissionPolicies = append(validatingAdmissionPolicies, admissionPoliciesFromFile...) + vaps = append(vaps, vapsFromFile...) + vapBindings = append(vapBindings, vapBindingsFromFile...) } } - return nil, nil, skipInvalidPolicies, nil, policies, validatingAdmissionPolicies, nil + return nil, nil, skipInvalidPolicies, nil, policies, vaps, vapBindings, nil } func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error, dclient.Interface) { diff --git a/cmd/cli/kubectl-kyverno/commands/fix/policy/options.go b/cmd/cli/kubectl-kyverno/commands/fix/policy/options.go index f7ceed5bc5..c8f3e20320 100644 --- a/cmd/cli/kubectl-kyverno/commands/fix/policy/options.go +++ b/cmd/cli/kubectl-kyverno/commands/fix/policy/options.go @@ -61,7 +61,7 @@ func (o options) execute(out io.Writer, dirs ...string) error { } func (o options) processFile(out io.Writer, path string) { - policies, vaps, err := policy.LoadWithLoader(policy.KubectlValidateLoader, nil, "", path) + policies, vaps, vapBindings, err := policy.LoadWithLoader(policy.KubectlValidateLoader, nil, "", path) if err != nil { return } @@ -169,6 +169,16 @@ func (o options) processFile(out io.Writer, path string) { yamlBytes = append(yamlBytes, []byte("---\n")...) yamlBytes = append(yamlBytes, finalBytes...) } + for _, vapBinding := range vapBindings { + finalBytes, err := yaml.Marshal(vapBinding) + if err != nil { + fmt.Fprintf(out, " ERROR: converting to yaml: %s", err) + fmt.Fprintln(out) + return + } + yamlBytes = append(yamlBytes, []byte("---\n")...) + yamlBytes = append(yamlBytes, finalBytes...) + } if err := os.WriteFile(path, yamlBytes, os.ModePerm); err != nil { fmt.Fprintf(out, " ERROR: saving file (%s): %s", path, err) fmt.Fprintln(out) diff --git a/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go b/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go index 181ff811d3..8958e1265e 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go @@ -86,7 +86,7 @@ func (o options) execute(ctx context.Context, dir string, keychain authn.Keychai if err != nil { return fmt.Errorf("reading layer blob: %v", err) } - policies, _, err := yamlutils.GetPolicy(layerBytes) + policies, _, _, err := yamlutils.GetPolicy(layerBytes) if err != nil { return fmt.Errorf("unmarshaling layer blob: %v", err) } diff --git a/cmd/cli/kubectl-kyverno/commands/oci/push/options.go b/cmd/cli/kubectl-kyverno/commands/oci/push/options.go index 0d626ff3fa..f824dc1d71 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/push/options.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/push/options.go @@ -35,7 +35,7 @@ func (o options) validate(policy string) error { } func (o options) execute(ctx context.Context, dir string, keychain authn.Keychain) error { - policies, _, err := policy.Load(nil, "", dir) + policies, _, _, err := policy.Load(nil, "", dir) if err != nil { return fmt.Errorf("unable to read policy file or directory %s (%w)", dir, err) } diff --git a/cmd/cli/kubectl-kyverno/commands/test/test.go b/cmd/cli/kubectl-kyverno/commands/test/test.go index 44f3454dfb..eb72563e22 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/test.go +++ b/cmd/cli/kubectl-kyverno/commands/test/test.go @@ -57,7 +57,7 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa // policies fmt.Fprintln(out, " Loading policies", "...") policyFullPath := path.GetFullPaths(testCase.Test.Policies, testDir, isGit) - policies, validatingAdmissionPolicies, err := policy.Load(testCase.Fs, testDir, policyFullPath...) + policies, validatingAdmissionPolicies, _, err := policy.Load(testCase.Fs, testDir, policyFullPath...) if err != nil { return nil, fmt.Errorf("Error: failed to load policies (%s)", err) } diff --git a/cmd/cli/kubectl-kyverno/policy/load.go b/cmd/cli/kubectl-kyverno/policy/load.go index 19cf57c1c7..7edd8b2fda 100644 --- a/cmd/cli/kubectl-kyverno/policy/load.go +++ b/cmd/cli/kubectl-kyverno/policy/load.go @@ -35,11 +35,13 @@ var ( policyV2 = schema.GroupVersion(kyvernov2beta1.GroupVersion).WithKind("Policy") clusterPolicyV1 = schema.GroupVersion(kyvernov1.GroupVersion).WithKind("ClusterPolicy") clusterPolicyV2 = schema.GroupVersion(kyvernov2beta1.GroupVersion).WithKind("ClusterPolicy") - vapV1Alpha1 = v1alpha1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy") + vapV1alpha1 = v1alpha1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy") vapV1Beta1 = v1beta1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy") + vapBidningV1alpha1 = v1alpha1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicyBinding") + vapBidningV1beta1 = v1beta1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicyBinding") LegacyLoader = yamlutils.GetPolicy KubectlValidateLoader = kubectlValidateLoader - defaultLoader = func(bytes []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { + defaultLoader = func(bytes []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { if experimental.UseKubectlValidate() { return KubectlValidateLoader(bytes) } else { @@ -48,159 +50,174 @@ var ( } ) -type loader = func([]byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) +type loader = func([]byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) -func Load(fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func Load(fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { return LoadWithLoader(nil, fs, resourcePath, paths...) } -func LoadWithLoader(loader loader, fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func LoadWithLoader(loader loader, fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { if loader == nil { loader = defaultLoader } var pols []kyvernov1.PolicyInterface var vaps []v1alpha1.ValidatingAdmissionPolicy + var vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding for _, path := range paths { if source.IsStdin(path) { - p, v, err := stdinLoad(loader) + p, v, b, err := stdinLoad(loader) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pols = append(pols, p...) vaps = append(vaps, v...) + vapBindings = append(vapBindings, b...) } else if fs != nil { - p, v, err := gitLoad(loader, fs, filepath.Join(resourcePath, path)) + p, v, b, err := gitLoad(loader, fs, filepath.Join(resourcePath, path)) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pols = append(pols, p...) vaps = append(vaps, v...) + vapBindings = append(vapBindings, b...) } else if source.IsHttp(path) { - p, v, err := httpLoad(loader, path) + p, v, b, err := httpLoad(loader, path) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pols = append(pols, p...) vaps = append(vaps, v...) + vapBindings = append(vapBindings, b...) } else { - p, v, err := fsLoad(loader, path) + p, v, b, err := fsLoad(loader, path) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pols = append(pols, p...) vaps = append(vaps, v...) + vapBindings = append(vapBindings, b...) } } - return pols, vaps, nil + return pols, vaps, vapBindings, nil } -func kubectlValidateLoader(content []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func kubectlValidateLoader(content []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { documents, err := extyaml.SplitDocuments(content) if err != nil { - return nil, nil, err + return nil, nil, nil, err } var policies []kyvernov1.PolicyInterface var vaps []v1alpha1.ValidatingAdmissionPolicy + var vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding for _, document := range documents { gvk, untyped, err := factory.Load(document) if err != nil { - return nil, nil, err + return nil, nil, nil, err } switch gvk { case policyV1, policyV2: typed, err := convert.To[kyvernov1.Policy](untyped) if err != nil { - return nil, nil, err + return nil, nil, nil, err } policies = append(policies, typed) case clusterPolicyV1, clusterPolicyV2: typed, err := convert.To[kyvernov1.ClusterPolicy](untyped) if err != nil { - return nil, nil, err + return nil, nil, nil, err } policies = append(policies, typed) - case vapV1Alpha1, vapV1Beta1: + case vapV1alpha1, vapV1Beta1: typed, err := convert.To[v1alpha1.ValidatingAdmissionPolicy](untyped) if err != nil { - return nil, nil, err + return nil, nil, nil, err } vaps = append(vaps, *typed) + case vapBidningV1alpha1, vapBidningV1beta1: + typed, err := convert.To[v1alpha1.ValidatingAdmissionPolicyBinding](untyped) + if err != nil { + return nil, nil, nil, err + } + vapBindings = append(vapBindings, *typed) default: - return nil, nil, fmt.Errorf("policy type not supported %s", gvk) + return nil, nil, nil, fmt.Errorf("policy type not supported %s", gvk) } } - return policies, vaps, nil + return policies, vaps, vapBindings, nil } -func fsLoad(loader loader, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func fsLoad(loader loader, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { var pols []kyvernov1.PolicyInterface var vaps []v1alpha1.ValidatingAdmissionPolicy + var vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding fi, err := os.Stat(filepath.Clean(path)) if err != nil { - return nil, nil, err + return nil, nil, nil, err } if fi.IsDir() { files, err := os.ReadDir(path) if err != nil { - return nil, nil, err + return nil, nil, nil, err } for _, file := range files { - p, v, err := fsLoad(loader, filepath.Join(path, file.Name())) + p, v, b, err := fsLoad(loader, filepath.Join(path, file.Name())) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pols = append(pols, p...) vaps = append(vaps, v...) + vapBindings = append(vapBindings, b...) } } else if git.IsYaml(fi) { fileBytes, err := os.ReadFile(filepath.Clean(path)) // #nosec G304 if err != nil { - return nil, nil, err + return nil, nil, nil, err } - p, v, err := loader(fileBytes) + p, v, b, err := loader(fileBytes) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pols = append(pols, p...) vaps = append(vaps, v...) + vapBindings = append(vapBindings, b...) } - return pols, vaps, nil + return pols, vaps, vapBindings, nil } -func httpLoad(loader loader, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func httpLoad(loader loader, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { // We accept here that a random URL might be called based on user provided input. req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, path, nil) if err != nil { - return nil, nil, fmt.Errorf("failed to process %v: %v", path, err) + return nil, nil, nil, fmt.Errorf("failed to process %v: %v", path, err) } resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, nil, fmt.Errorf("failed to process %v: %v", path, err) + return nil, nil, nil, fmt.Errorf("failed to process %v: %v", path, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("failed to process %v: %v", path, err) + return nil, nil, nil, fmt.Errorf("failed to process %v: %v", path, err) } fileBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to process %v: %v", path, err) + return nil, nil, nil, fmt.Errorf("failed to process %v: %v", path, err) } return loader(fileBytes) } -func gitLoad(loader loader, fs billy.Filesystem, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func gitLoad(loader loader, fs billy.Filesystem, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { file, err := fs.Open(path) if err != nil { - return nil, nil, err + return nil, nil, nil, err } fileBytes, err := io.ReadAll(file) if err != nil { - return nil, nil, err + return nil, nil, nil, err } return loader(fileBytes) } -func stdinLoad(loader loader) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func stdinLoad(loader loader) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { policyStr := "" scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { diff --git a/cmd/cli/kubectl-kyverno/policy/load_test.go b/cmd/cli/kubectl-kyverno/policy/load_test.go index ba095decde..cf67d8b11c 100644 --- a/cmd/cli/kubectl-kyverno/policy/load_test.go +++ b/cmd/cli/kubectl-kyverno/policy/load_test.go @@ -31,7 +31,7 @@ func TestLoad(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, _, err := Load(tt.fs, tt.resourcePath, tt.paths...) + _, _, _, err := Load(tt.fs, tt.resourcePath, tt.paths...) if (err != nil) != tt.wantErr { t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) return @@ -87,7 +87,7 @@ func TestLoadWithKubectlValidate(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - policies, vaps, err := LoadWithLoader(KubectlValidateLoader, tt.fs, tt.resourcePath, tt.paths...) + policies, vaps, _, err := LoadWithLoader(KubectlValidateLoader, tt.fs, tt.resourcePath, tt.paths...) if (err != nil) != tt.wantErr { t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/cmd/cli/kubectl-kyverno/policy/variables_test.go b/cmd/cli/kubectl-kyverno/policy/variables_test.go index 8fa69036a4..c80a6712f7 100644 --- a/cmd/cli/kubectl-kyverno/policy/variables_test.go +++ b/cmd/cli/kubectl-kyverno/policy/variables_test.go @@ -11,7 +11,7 @@ import ( func TestExtractVariables(t *testing.T) { loadPolicy := func(path string) kyvernov1.PolicyInterface { t.Helper() - policies, _, err := Load(nil, "", path) + policies, _, _, err := Load(nil, "", path) assert.NoError(t, err) assert.Equal(t, len(policies), 1) return policies[0] diff --git a/cmd/cli/kubectl-kyverno/processor/policy_processor_test.go b/cmd/cli/kubectl-kyverno/processor/policy_processor_test.go index bf65fc4f66..f14d5bb690 100644 --- a/cmd/cli/kubectl-kyverno/processor/policy_processor_test.go +++ b/cmd/cli/kubectl-kyverno/processor/policy_processor_test.go @@ -105,7 +105,7 @@ func Test_NamespaceSelector(t *testing.T) { } rc := &ResultCounts{} for _, tc := range testcases { - policyArray, _, _ := yamlutils.GetPolicy(tc.policy) + policyArray, _, _, _ := yamlutils.GetPolicy(tc.policy) resourceArray, _ := resource.GetUnstructuredResources(tc.resource) processor := PolicyProcessor{ Store: &store.Store{}, diff --git a/cmd/cli/kubectl-kyverno/processor/vap_processor.go b/cmd/cli/kubectl-kyverno/processor/vap_processor.go index 96f522650f..fc294d32ef 100644 --- a/cmd/cli/kubectl-kyverno/processor/vap_processor.go +++ b/cmd/cli/kubectl-kyverno/processor/vap_processor.go @@ -1,6 +1,7 @@ package processor import ( + "github.com/kyverno/kyverno/pkg/clients/dclient" engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/validatingadmissionpolicy" "k8s.io/api/admissionregistration/v1alpha1" @@ -9,15 +10,23 @@ import ( type ValidatingAdmissionPolicyProcessor struct { Policies []v1alpha1.ValidatingAdmissionPolicy + Bindings []v1alpha1.ValidatingAdmissionPolicyBinding Resource *unstructured.Unstructured PolicyReport bool Rc *ResultCounts + Client dclient.Interface } func (p *ValidatingAdmissionPolicyProcessor) ApplyPolicyOnResource() ([]engineapi.EngineResponse, error) { var responses []engineapi.EngineResponse for _, policy := range p.Policies { - response := validatingadmissionpolicy.Validate(policy, *p.Resource) + policyData := validatingadmissionpolicy.NewPolicyData(policy) + for _, binding := range p.Bindings { + if binding.Spec.PolicyName == policy.Name { + policyData.AddBinding(binding) + } + } + response, _ := validatingadmissionpolicy.Validate(policyData, *p.Resource, p.Client) responses = append(responses, response) p.Rc.addValidatingAdmissionResponse(policy, response) } diff --git a/cmd/cli/kubectl-kyverno/report/report_test.go b/cmd/cli/kubectl-kyverno/report/report_test.go index a1570ae356..ec6f917398 100644 --- a/cmd/cli/kubectl-kyverno/report/report_test.go +++ b/cmd/cli/kubectl-kyverno/report/report_test.go @@ -15,7 +15,7 @@ import ( ) func TestComputeClusterPolicyReports(t *testing.T) { - policies, _, err := policy.Load(nil, "", "../_testdata/policies/cpol-pod-requirements.yaml") + policies, _, _, err := policy.Load(nil, "", "../_testdata/policies/cpol-pod-requirements.yaml") assert.NilError(t, err) assert.Equal(t, len(policies), 1) policy := policies[0] @@ -49,7 +49,7 @@ func TestComputeClusterPolicyReports(t *testing.T) { } func TestComputePolicyReports(t *testing.T) { - policies, _, err := policy.Load(nil, "", "../_testdata/policies/pol-pod-requirements.yaml") + policies, _, _, err := policy.Load(nil, "", "../_testdata/policies/pol-pod-requirements.yaml") assert.NilError(t, err) assert.Equal(t, len(policies), 1) policy := policies[0] @@ -84,7 +84,7 @@ func TestComputePolicyReports(t *testing.T) { } func TestComputePolicyReportResultsPerPolicyOld(t *testing.T) { - policies, _, err := policy.Load(nil, "", "../_testdata/policies/cpol-pod-requirements.yaml") + policies, _, _, err := policy.Load(nil, "", "../_testdata/policies/cpol-pod-requirements.yaml") assert.NilError(t, err) assert.Equal(t, len(policies), 1) policy := policies[0] @@ -162,7 +162,7 @@ func TestMergeClusterReport(t *testing.T) { } func TestComputePolicyReportResult(t *testing.T) { - policies, _, err := policy.Load(nil, "", "../_testdata/policies/cpol-pod-requirements.yaml") + policies, _, _, err := policy.Load(nil, "", "../_testdata/policies/cpol-pod-requirements.yaml") assert.NilError(t, err) assert.Equal(t, len(policies), 1) policy := policies[0] diff --git a/pkg/autogen/autogen_test.go b/pkg/autogen/autogen_test.go index 8e80bf562e..00e7e3c09d 100644 --- a/pkg/autogen/autogen_test.go +++ b/pkg/autogen/autogen_test.go @@ -569,7 +569,7 @@ kA== for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - policies, _, err := yamlutils.GetPolicy([]byte(test.policy)) + policies, _, _, err := yamlutils.GetPolicy([]byte(test.policy)) assert.NilError(t, err) assert.Equal(t, 1, len(policies)) rules := computeRules(policies[0]) @@ -580,7 +580,7 @@ kA== func Test_PodSecurityWithNoExceptions(t *testing.T) { policy := []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"pod-security"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"restricted","match":{"all":[{"resources":{"kinds":["Pod"]}}]},"validate":{"podSecurity":{"level":"restricted","version":"v1.24"}}}]}}`) - policies, _, err := yamlutils.GetPolicy([]byte(policy)) + policies, _, _, err := yamlutils.GetPolicy([]byte(policy)) assert.NilError(t, err) assert.Equal(t, 1, len(policies)) @@ -628,7 +628,7 @@ func Test_ValidateWithCELExpressions(t *testing.T) { } } `) - policies, _, err := yamlutils.GetPolicy([]byte(policy)) + policies, _, _, err := yamlutils.GetPolicy([]byte(policy)) assert.NilError(t, err) assert.Equal(t, 1, len(policies)) diff --git a/pkg/controllers/report/background/controller.go b/pkg/controllers/report/background/controller.go index 3d96fff590..d97912cbfb 100644 --- a/pkg/controllers/report/background/controller.go +++ b/pkg/controllers/report/background/controller.go @@ -361,7 +361,7 @@ func (c *controller) reconcileReport( } } if full || reevaluate || actual[reportutils.PolicyLabel(policy)] != policy.GetResourceVersion() { - scanner := utils.NewScanner(logger, c.engine, c.config, c.jp) + scanner := utils.NewScanner(logger, c.engine, c.config, c.jp, c.client) for _, result := range scanner.ScanResource(ctx, *target, nsLabels, policy) { if result.Error != nil { return result.Error diff --git a/pkg/controllers/report/utils/scanner.go b/pkg/controllers/report/utils/scanner.go index 851d016b98..12238f838a 100644 --- a/pkg/controllers/report/utils/scanner.go +++ b/pkg/controllers/report/utils/scanner.go @@ -6,6 +6,7 @@ import ( "github.com/go-logr/logr" "github.com/kyverno/kyverno/api/kyverno" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + "github.com/kyverno/kyverno/pkg/clients/dclient" "github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/engine" engineapi "github.com/kyverno/kyverno/pkg/engine/api" @@ -20,6 +21,7 @@ type scanner struct { engine engineapi.Engine config config.Configuration jp jmespath.Interface + client dclient.Interface } type ScanResult struct { @@ -36,12 +38,14 @@ func NewScanner( engine engineapi.Engine, config config.Configuration, jp jmespath.Interface, + client dclient.Interface, ) Scanner { return &scanner{ logger: logger, engine: engine, config: config, jp: jp, + client: client, } } @@ -74,7 +78,11 @@ func (s *scanner) ScanResource(ctx context.Context, resource unstructured.Unstru } } else { pol := policy.AsValidatingAdmissionPolicy() - res := validatingadmissionpolicy.Validate(*pol, resource) + policyData := validatingadmissionpolicy.NewPolicyData(*pol) + res, err := validatingadmissionpolicy.Validate(policyData, resource, s.client) + if err != nil { + errors = append(errors, err) + } response = &res } results[&policies[i]] = ScanResult{response, multierr.Combine(errors...)} diff --git a/pkg/controllers/validatingadmissionpolicy-generate/policycheck_test.go b/pkg/controllers/validatingadmissionpolicy-generate/policycheck_test.go index 4f9240db86..c13e8cc7fd 100644 --- a/pkg/controllers/validatingadmissionpolicy-generate/policycheck_test.go +++ b/pkg/controllers/validatingadmissionpolicy-generate/policycheck_test.go @@ -477,7 +477,7 @@ func Test_Can_Generate_ValidatingAdmissionPolicy(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - policies, _, err := yamlutils.GetPolicy([]byte(test.policy)) + policies, _, _, err := yamlutils.GetPolicy([]byte(test.policy)) assert.NilError(t, err) assert.Equal(t, 1, len(policies)) out, _ := canGenerateVAP(policies[0].GetSpec()) diff --git a/pkg/utils/yaml/loadpolicy.go b/pkg/utils/yaml/loadpolicy.go index e66c4e606f..d0027e22e8 100644 --- a/pkg/utils/yaml/loadpolicy.go +++ b/pkg/utils/yaml/loadpolicy.go @@ -15,58 +15,71 @@ import ( ) // GetPolicy extracts policies from YAML bytes -func GetPolicy(bytes []byte) (policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, err error) { +func GetPolicy(bytes []byte) (policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, validatingAdmissionPolicyBindings []v1alpha1.ValidatingAdmissionPolicyBinding, err error) { documents, err := extyaml.SplitDocuments(bytes) if err != nil { - return nil, nil, err + return nil, nil, nil, err } for _, thisPolicyBytes := range documents { policyBytes, err := yaml.ToJSON(thisPolicyBytes) if err != nil { - return nil, nil, fmt.Errorf("failed to convert to JSON: %v", err) + return nil, nil, nil, fmt.Errorf("failed to convert to JSON: %v", err) } us := &unstructured.Unstructured{} if err := json.Unmarshal(policyBytes, us); err != nil { - return nil, nil, fmt.Errorf("failed to decode policy: %v", err) + return nil, nil, nil, fmt.Errorf("failed to decode policy: %v", err) } if us.IsList() { list, err := us.ToList() if err != nil { - return nil, nil, fmt.Errorf("failed to decode policy list: %v", err) + return nil, nil, nil, fmt.Errorf("failed to decode policy list: %v", err) } for i := range list.Items { item := list.Items[i] - if policies, validatingAdmissionPolicies, err = addPolicy(policies, validatingAdmissionPolicies, &item); err != nil { - return nil, nil, err + if policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, err = addPolicy(policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, &item); err != nil { + return nil, nil, nil, err } } } else { - if policies, validatingAdmissionPolicies, err = addPolicy(policies, validatingAdmissionPolicies, us); err != nil { - return nil, nil, err + if policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, err = addPolicy(policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, us); err != nil { + return nil, nil, nil, err } } } - return policies, validatingAdmissionPolicies, nil + return policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, err } -func addPolicy(policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, us *unstructured.Unstructured) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { +func addPolicy(policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy, validatingAdmissionPolicyBindings []v1alpha1.ValidatingAdmissionPolicyBinding, us *unstructured.Unstructured) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { kind := us.GetKind() if strings.Compare(kind, "ValidatingAdmissionPolicy") == 0 { validatingAdmissionPolicy := v1alpha1.ValidatingAdmissionPolicy{} if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(us.Object, &validatingAdmissionPolicy, true); err != nil { - return policies, nil, fmt.Errorf("failed to decode policy: %v", err) + return policies, nil, validatingAdmissionPolicyBindings, fmt.Errorf("failed to decode policy: %v", err) } if validatingAdmissionPolicy.Kind == "" { log.V(3).Info("skipping file as ValidatingAdmissionPolicy.Kind not found") - return policies, validatingAdmissionPolicies, nil + return policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, nil } validatingAdmissionPolicies = append(validatingAdmissionPolicies, validatingAdmissionPolicy) + } else if strings.Compare(kind, "ValidatingAdmissionPolicyBinding") == 0 { + validatingAdmissionPolicyBinding := v1alpha1.ValidatingAdmissionPolicyBinding{} + + if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(us.Object, &validatingAdmissionPolicyBinding, true); err != nil { + return policies, validatingAdmissionPolicies, nil, fmt.Errorf("failed to decode policy: %v", err) + } + + if validatingAdmissionPolicyBinding.Kind == "" { + log.V(3).Info("skipping file as ValidatingAdmissionPolicyBinding.Kind not found") + return policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, nil + } + + validatingAdmissionPolicyBindings = append(validatingAdmissionPolicyBindings, validatingAdmissionPolicyBinding) } else { var policy kyvernov1.PolicyInterface if us.GetKind() == "ClusterPolicy" { @@ -74,19 +87,19 @@ func addPolicy(policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies } else if us.GetKind() == "Policy" { policy = &kyvernov1.Policy{} } else { - return policies, validatingAdmissionPolicies, nil + return policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, nil } if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(us.Object, policy, true); err != nil { - return nil, validatingAdmissionPolicies, fmt.Errorf("failed to decode policy: %v", err) + return nil, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, fmt.Errorf("failed to decode policy: %v", err) } if policy.GetKind() == "" { log.V(3).Info("skipping file as policy.TypeMeta.Kind not found") - return policies, validatingAdmissionPolicies, nil + return policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, nil } if policy.GetKind() != "ClusterPolicy" && policy.GetKind() != "Policy" { - return nil, validatingAdmissionPolicies, fmt.Errorf("resource %s/%s is not a Policy or a ClusterPolicy", policy.GetKind(), policy.GetName()) + return nil, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, fmt.Errorf("resource %s/%s is not a Policy or a ClusterPolicy", policy.GetKind(), policy.GetName()) } if policy.GetKind() == "Policy" { @@ -99,5 +112,5 @@ func addPolicy(policies []kyvernov1.PolicyInterface, validatingAdmissionPolicies policies = append(policies, policy) } - return policies, validatingAdmissionPolicies, nil + return policies, validatingAdmissionPolicies, validatingAdmissionPolicyBindings, nil } diff --git a/pkg/utils/yaml/loadpolicy_test.go b/pkg/utils/yaml/loadpolicy_test.go index 00222107a7..11e37b727a 100644 --- a/pkg/utils/yaml/loadpolicy_test.go +++ b/pkg/utils/yaml/loadpolicy_test.go @@ -15,11 +15,12 @@ func TestGetPolicy(t *testing.T) { namespace string } tests := []struct { - name string - args args - wantPolicies []policy - validatingAdmissionPolicies []policy - wantErr bool + name string + args args + wantPolicies []policy + vaps []policy + vapBindings []policy + wantErr bool }{{ name: "policy", args: args{ @@ -317,7 +318,7 @@ spec: validations: - expression: "object.spec.replicas <= 5" `), - }, validatingAdmissionPolicies: []policy{ + }, vaps: []policy{ {"ValidatingAdmissionPolicy", ""}, }, wantErr: false, @@ -371,7 +372,7 @@ spec: }, wantPolicies: []policy{ {"Policy", "ns-1"}, }, - validatingAdmissionPolicies: []policy{ + vaps: []policy{ {"ValidatingAdmissionPolicy", ""}, }, wantErr: false, @@ -424,14 +425,71 @@ spec: }, wantPolicies: []policy{ {"ClusterPolicy", ""}, }, - validatingAdmissionPolicies: []policy{ + vaps: []policy{ {"ValidatingAdmissionPolicy", ""}, }, wantErr: false, + }, { + name: "ValidatingAdmissionPolicyBinding", + args: args{ + []byte(` +apiVersion: admissionregistration.k8s.io/v1alpha1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "demo-binding-test.example.com" +spec: + policyName: "demo-policy.example.com" + validationActions: [Deny] + matchResources: + namespaceSelector: + matchLabels: + environment: test +`), + }, vapBindings: []policy{ + {"ValidatingAdmissionPolicyBinding", ""}, + }, + wantErr: false, + }, { + name: "ValidatingAdmissionPolicy and its binding", + args: args{ + []byte(` +apiVersion: admissionregistration.k8s.io/v1alpha1 +kind: ValidatingAdmissionPolicy +metadata: + name: "demo-policy.example.com" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + validations: + - expression: "object.spec.replicas <= 5" +--- +apiVersion: admissionregistration.k8s.io/v1alpha1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "demo-binding-test.example.com" +spec: + policyName: "demo-policy.example.com" + validationActions: [Deny] + matchResources: + namespaceSelector: + matchLabels: + environment: test +`), + }, vaps: []policy{ + {"ValidatingAdmissionPolicy", ""}, + }, vapBindings: []policy{ + {"ValidatingAdmissionPolicyBinding", ""}, + }, + wantErr: false, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPolicies, gotValidatingAdmissionPolicies, err := GetPolicy(tt.args.bytes) + gotPolicies, gotValidatingAdmissionPolicies, gotBindings, err := GetPolicy(tt.args.bytes) if tt.wantErr { assert.Error(t, err) } else { @@ -443,12 +501,17 @@ spec: } } - if assert.Equal(t, len(tt.validatingAdmissionPolicies), len(gotValidatingAdmissionPolicies)) { - for i := range tt.validatingAdmissionPolicies { - assert.Equal(t, tt.validatingAdmissionPolicies[i].kind, gotValidatingAdmissionPolicies[i].Kind) + if assert.Equal(t, len(tt.vaps), len(gotValidatingAdmissionPolicies)) { + for i := range tt.vaps { + assert.Equal(t, tt.vaps[i].kind, gotValidatingAdmissionPolicies[i].Kind) } } + if assert.Equal(t, len(tt.vapBindings), len(gotBindings)) { + for i := range tt.vapBindings { + assert.Equal(t, tt.vapBindings[i].kind, gotBindings[i].Kind) + } + } } }) } diff --git a/pkg/validatingadmissionpolicy/api.go b/pkg/validatingadmissionpolicy/api.go new file mode 100644 index 0000000000..076ed001cc --- /dev/null +++ b/pkg/validatingadmissionpolicy/api.go @@ -0,0 +1,68 @@ +package validatingadmissionpolicy + +import ( + "context" + + "github.com/kyverno/kyverno/pkg/clients/dclient" + "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + corev1listers "k8s.io/client-go/listers/core/v1" +) + +// Everything someone might need to validate a single ValidatingPolicyDefinition +// against all of its registered bindings. +type PolicyData struct { + definition v1alpha1.ValidatingAdmissionPolicy + bindings []v1alpha1.ValidatingAdmissionPolicyBinding +} + +func (p *PolicyData) AddBinding(binding v1alpha1.ValidatingAdmissionPolicyBinding) { + p.bindings = append(p.bindings, binding) +} + +func (p *PolicyData) GetDefinition() v1alpha1.ValidatingAdmissionPolicy { + return p.definition +} + +func (p *PolicyData) GetBindings() []v1alpha1.ValidatingAdmissionPolicyBinding { + return p.bindings +} + +func NewPolicyData(policy v1alpha1.ValidatingAdmissionPolicy) PolicyData { + return PolicyData{ + definition: policy, + } +} + +type CustomNamespaceLister struct { + dClient dclient.Interface +} + +func (c *CustomNamespaceLister) List(selector labels.Selector) (ret []*corev1.Namespace, err error) { + var namespaces []*corev1.Namespace + namespace, err := c.dClient.GetKubeClient().CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, ns := range namespace.Items { + nsCopy := ns + namespaces = append(namespaces, &nsCopy) + } + return namespaces, nil +} + +func (c *CustomNamespaceLister) Get(name string) (*corev1.Namespace, error) { + namespace, err := c.dClient.GetKubeClient().CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return namespace, nil +} + +func NewCustomNamespaceLister(dClient dclient.Interface) corev1listers.NamespaceLister { + return &CustomNamespaceLister{ + dClient: dClient, + } +} diff --git a/pkg/validatingadmissionpolicy/utils.go b/pkg/validatingadmissionpolicy/utils.go new file mode 100644 index 0000000000..ac14f11f65 --- /dev/null +++ b/pkg/validatingadmissionpolicy/utils.go @@ -0,0 +1,54 @@ +package validatingadmissionpolicy + +import ( + "k8s.io/api/admissionregistration/v1alpha1" + "k8s.io/api/admissionregistration/v1beta1" +) + +func convertRules(v1alpha1rules []v1alpha1.NamedRuleWithOperations) []v1beta1.NamedRuleWithOperations { + var v1beta1rules []v1beta1.NamedRuleWithOperations + for _, r := range v1alpha1rules { + v1beta1rules = append(v1beta1rules, v1beta1.NamedRuleWithOperations(r)) + } + return v1beta1rules +} + +func convertValidations(v1alpha1validations []v1alpha1.Validation) []v1beta1.Validation { + var v1beta1validations []v1beta1.Validation + for _, v := range v1alpha1validations { + v1beta1validations = append(v1beta1validations, v1beta1.Validation(v)) + } + return v1beta1validations +} + +func convertAuditAnnotations(v1alpha1auditanns []v1alpha1.AuditAnnotation) []v1beta1.AuditAnnotation { + var v1beta1auditanns []v1beta1.AuditAnnotation + for _, a := range v1alpha1auditanns { + v1beta1auditanns = append(v1beta1auditanns, v1beta1.AuditAnnotation(a)) + } + return v1beta1auditanns +} + +func convertMatchConditions(v1alpha1conditions []v1alpha1.MatchCondition) []v1beta1.MatchCondition { + var v1beta1conditions []v1beta1.MatchCondition + for _, m := range v1alpha1conditions { + v1beta1conditions = append(v1beta1conditions, v1beta1.MatchCondition(m)) + } + return v1beta1conditions +} + +func convertVariables(v1alpha1variables []v1alpha1.Variable) []v1beta1.Variable { + var v1beta1variables []v1beta1.Variable + for _, v := range v1alpha1variables { + v1beta1variables = append(v1beta1variables, v1beta1.Variable(v)) + } + return v1beta1variables +} + +func convertValidationActions(v1alpha1actions []v1alpha1.ValidationAction) []v1beta1.ValidationAction { + var v1beta1actions []v1beta1.ValidationAction + for _, a := range v1alpha1actions { + v1beta1actions = append(v1beta1actions, v1beta1.ValidationAction(a)) + } + return v1beta1actions +} diff --git a/pkg/validatingadmissionpolicy/validatingadmissionpolicy.go b/pkg/validatingadmissionpolicy/validatingadmissionpolicy.go index 7610a56df2..43bb4a61d1 100644 --- a/pkg/validatingadmissionpolicy/validatingadmissionpolicy.go +++ b/pkg/validatingadmissionpolicy/validatingadmissionpolicy.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/kyverno/kyverno/pkg/clients/dclient" engineapi "github.com/kyverno/kyverno/pkg/engine/api" celutils "github.com/kyverno/kyverno/pkg/utils/cel" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" @@ -13,11 +14,15 @@ import ( "golang.org/x/text/language" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/api/admissionregistration/v1alpha1" + "k8s.io/api/admissionregistration/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" celconfig "k8s.io/apiserver/pkg/apis/cel" ) @@ -60,18 +65,143 @@ func GetKinds(policy v1alpha1.ValidatingAdmissionPolicy) []string { return kindList } -func Validate(policy v1alpha1.ValidatingAdmissionPolicy, resource unstructured.Unstructured) engineapi.EngineResponse { - resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName()) - logger.V(3).Info("applying policy on resource", "policy", policy.GetName(), "resource", resPath) +func Validate(policyData PolicyData, resource unstructured.Unstructured, client dclient.Interface) (engineapi.EngineResponse, error) { + var ( + gvr schema.GroupVersionResource + a admission.Attributes + err error + ) + policy := policyData.definition + bindings := policyData.bindings + engineResponse := engineapi.NewEngineResponse(resource, engineapi.NewValidatingAdmissionPolicy(policy), nil) + if client != nil { + nsLister := NewCustomNamespaceLister(client) + matcher := validatingadmissionpolicy.NewMatcher(matching.NewMatcher(nsLister, client.GetKubeClient())) + + // convert policy from v1alpha1 to v1beta1 + var namespaceSelector, objectSelector metav1.LabelSelector + if policy.Spec.MatchConstraints.NamespaceSelector != nil { + namespaceSelector = *policy.Spec.MatchConstraints.NamespaceSelector + } + if policy.Spec.MatchConstraints.ObjectSelector != nil { + objectSelector = *policy.Spec.MatchConstraints.ObjectSelector + } + v1beta1policy := &v1beta1.ValidatingAdmissionPolicy{ + Spec: v1beta1.ValidatingAdmissionPolicySpec{ + FailurePolicy: (*v1beta1.FailurePolicyType)(policy.Spec.FailurePolicy), + ParamKind: (*v1beta1.ParamKind)(policy.Spec.ParamKind), + MatchConstraints: &v1beta1.MatchResources{ + NamespaceSelector: &namespaceSelector, + ObjectSelector: &objectSelector, + ResourceRules: convertRules(policy.Spec.MatchConstraints.ResourceRules), + ExcludeResourceRules: convertRules(policy.Spec.MatchConstraints.ExcludeResourceRules), + MatchPolicy: (*v1beta1.MatchPolicyType)(policy.Spec.MatchConstraints.MatchPolicy), + }, + Validations: convertValidations(policy.Spec.Validations), + AuditAnnotations: convertAuditAnnotations(policy.Spec.AuditAnnotations), + MatchConditions: convertMatchConditions(policy.Spec.MatchConditions), + Variables: convertVariables(policy.Spec.Variables), + }, + } + + // construct admission attributes + gvr, err = client.Discovery().GetGVRFromGVK(resource.GroupVersionKind()) + if err != nil { + return engineResponse, err + } + a = admission.NewAttributesRecord(resource.DeepCopyObject(), nil, resource.GroupVersionKind(), resource.GetNamespace(), resource.GetName(), gvr, "", admission.Create, nil, false, nil) + + // check if policy matches the incoming resource + o := admission.NewObjectInterfacesFromScheme(runtime.NewScheme()) + isMatch, _, _, err := matcher.DefinitionMatches(a, o, v1beta1policy) + if err != nil { + return engineResponse, err + } + if !isMatch { + return engineResponse, nil + } + + if len(bindings) == 0 { + a = admission.NewAttributesRecord(resource.DeepCopyObject(), nil, resource.GroupVersionKind(), resource.GetNamespace(), resource.GetName(), gvr, "", admission.Create, nil, false, nil) + resPath := fmt.Sprintf("%s/%s/%s", a.GetNamespace(), a.GetKind().Kind, a.GetName()) + logger.V(3).Info("validate resource %s against policy %s", resPath, policy.GetName()) + return validateResource(policy, resource, a) + } else { + for _, binding := range bindings { + // convert policy binding from v1alpha1 to v1beta1 + var namespaceSelector, objectSelector, paramSelector metav1.LabelSelector + if binding.Spec.MatchResources.NamespaceSelector != nil { + namespaceSelector = *binding.Spec.MatchResources.NamespaceSelector + } + if binding.Spec.MatchResources.ObjectSelector != nil { + objectSelector = *binding.Spec.MatchResources.ObjectSelector + } + + var paramRef v1beta1.ParamRef + if binding.Spec.ParamRef != nil { + paramRef.Name = binding.Spec.ParamRef.Name + paramRef.Namespace = binding.Spec.ParamRef.Namespace + if binding.Spec.ParamRef.Selector != nil { + paramRef.Selector = binding.Spec.ParamRef.Selector + } else { + paramRef.Selector = ¶mSelector + } + paramRef.ParameterNotFoundAction = (*v1beta1.ParameterNotFoundActionType)(binding.Spec.ParamRef.ParameterNotFoundAction) + } + + v1beta1binding := &v1beta1.ValidatingAdmissionPolicyBinding{ + Spec: v1beta1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: binding.Spec.PolicyName, + ParamRef: ¶mRef, + MatchResources: &v1beta1.MatchResources{ + NamespaceSelector: &namespaceSelector, + ObjectSelector: &objectSelector, + ResourceRules: convertRules(binding.Spec.MatchResources.ResourceRules), + ExcludeResourceRules: convertRules(binding.Spec.MatchResources.ExcludeResourceRules), + MatchPolicy: (*v1beta1.MatchPolicyType)(binding.Spec.MatchResources.MatchPolicy), + }, + ValidationActions: convertValidationActions(binding.Spec.ValidationActions), + }, + } + isMatch, err := matcher.BindingMatches(a, o, v1beta1binding) + if err != nil { + return engineResponse, err + } + if !isMatch { + continue + } + + resPath := fmt.Sprintf("%s/%s/%s", a.GetNamespace(), a.GetKind().Kind, a.GetName()) + logger.V(3).Info("validate resource %s against policy %s with binding %s", resPath, policy.GetName(), binding.GetName()) + return validateResource(policy, resource, a) + } + } + } else { + a = admission.NewAttributesRecord(resource.DeepCopyObject(), nil, resource.GroupVersionKind(), resource.GetNamespace(), resource.GetName(), gvr, "", admission.Create, nil, false, nil) + resPath := fmt.Sprintf("%s/%s/%s", a.GetNamespace(), a.GetKind().Kind, a.GetName()) + logger.V(3).Info("validate resource %s against policy %s", resPath, policy.GetName()) + return validateResource(policy, resource, a) + } + + return engineResponse, nil +} + +func validateResource(policy v1alpha1.ValidatingAdmissionPolicy, resource unstructured.Unstructured, a admission.Attributes) (engineapi.EngineResponse, error) { startTime := time.Now() - validations := policy.Spec.Validations - auditAnnotations := policy.Spec.AuditAnnotations - matchConditions := policy.Spec.MatchConditions - variables := policy.Spec.Variables + engineResponse := engineapi.NewEngineResponse(resource, engineapi.NewValidatingAdmissionPolicy(policy), nil) + policyResp := engineapi.NewPolicyResponse() + var ruleResp *engineapi.RuleResponse + // compile CEL expressions + compiler, err := celutils.NewCompiler(policy.Spec.Validations, policy.Spec.AuditAnnotations, policy.Spec.MatchConditions, policy.Spec.Variables) + if err != nil { + return engineResponse, err + } hasParam := policy.Spec.ParamKind != nil + optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false} + compiler.CompileVariables(optionalVars) var failPolicy admissionregistrationv1.FailurePolicyType if policy.Spec.FailurePolicy == nil { @@ -87,43 +217,16 @@ func Validate(policy v1alpha1.ValidatingAdmissionPolicy, resource unstructured.U matchPolicy = *policy.Spec.MatchConstraints.MatchPolicy } - engineResponse := engineapi.NewEngineResponse(resource, engineapi.NewValidatingAdmissionPolicy(policy), nil) - policyResp := engineapi.NewPolicyResponse() - var ruleResp *engineapi.RuleResponse - - optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false} - - // compile CEL expressions - compiler, err := celutils.NewCompiler(validations, auditAnnotations, matchConditions, variables) - if err != nil { - ruleResp = engineapi.RuleError(policy.GetName(), engineapi.Validation, "Error creating composited compiler", err) - policyResp.Add(engineapi.NewExecutionStats(startTime, time.Now()), *ruleResp) - engineResponse = engineResponse.WithPolicyResponse(policyResp) - return engineResponse - } - compiler.CompileVariables(optionalVars) - filter := compiler.CompileValidateExpressions(optionalVars) - messageExpressionfilter := compiler.CompileMessageExpressions(optionalVars) - auditAnnotationFilter := compiler.CompileAuditAnnotationsExpressions(optionalVars) - matchConditionFilter := compiler.CompileMatchExpressions(optionalVars) - - newMatcher := matchconditions.NewMatcher(matchConditionFilter, &failPolicy, "", string(matchPolicy), "") - validator := validatingadmissionpolicy.NewValidator(filter, newMatcher, auditAnnotationFilter, messageExpressionfilter, nil) - - admissionAttributes := admission.NewAttributesRecord( - resource.DeepCopyObject(), - nil, resource.GroupVersionKind(), - resource.GetNamespace(), - resource.GetName(), - schema.GroupVersionResource{}, - "", - admission.Create, - nil, - false, - nil, + newMatcher := matchconditions.NewMatcher(compiler.CompileMatchExpressions(optionalVars), &failPolicy, "", string(matchPolicy), "") + validator := validatingadmissionpolicy.NewValidator( + compiler.CompileValidateExpressions(optionalVars), + newMatcher, + compiler.CompileAuditAnnotationsExpressions(optionalVars), + compiler.CompileMessageExpressions(optionalVars), + &failPolicy, ) - versionedAttr, _ := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), nil) - validateResult := validator.Validate(context.TODO(), schema.GroupVersionResource{}, versionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil) + versionedAttr, _ := admission.NewVersionedAttributes(a, a.GetKind(), nil) + validateResult := validator.Validate(context.TODO(), a.GetResource(), versionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil) isPass := true for _, policyDecision := range validateResult.Decisions { @@ -144,5 +247,5 @@ func Validate(policy v1alpha1.ValidatingAdmissionPolicy, resource unstructured.U policyResp.Add(engineapi.NewExecutionStats(startTime, time.Now()), *ruleResp) engineResponse = engineResponse.WithPolicyResponse(policyResp) - return engineResponse + return engineResponse, nil } diff --git a/pkg/validatingadmissionpolicy/validatingadmissionpolicy_test.go b/pkg/validatingadmissionpolicy/validatingadmissionpolicy_test.go index 7efee5b3f0..30743a1627 100644 --- a/pkg/validatingadmissionpolicy/validatingadmissionpolicy_test.go +++ b/pkg/validatingadmissionpolicy/validatingadmissionpolicy_test.go @@ -127,7 +127,7 @@ spec: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, policy, _ := yamlutils.GetPolicy(tt.policy) + _, policy, _, _ := yamlutils.GetPolicy(tt.policy) kinds := GetKinds(policy[0]) if !reflect.DeepEqual(kinds, tt.wantKinds) { t.Errorf("Expected %v, got %v", tt.wantKinds, kinds) diff --git a/pkg/validation/policy/allowed_vars_test.go b/pkg/validation/policy/allowed_vars_test.go index e1ca6270ef..83267eaf4f 100644 --- a/pkg/validation/policy/allowed_vars_test.go +++ b/pkg/validation/policy/allowed_vars_test.go @@ -54,7 +54,7 @@ func TestNotAllowedVars_MatchSection(t *testing.T) { } `) - policy, _, err := yamlutils.GetPolicy(policyWithVarInMatch) + policy, _, _, err := yamlutils.GetPolicy(policyWithVarInMatch) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -106,7 +106,7 @@ func TestNotAllowedVars_ExcludeSection(t *testing.T) { } `) - policy, _, err := yamlutils.GetPolicy(policyWithVarInExclude) + policy, _, _, err := yamlutils.GetPolicy(policyWithVarInExclude) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -159,7 +159,7 @@ func TestNotAllowedVars_ExcludeSection_PositiveCase(t *testing.T) { } `) - policy, _, err := yamlutils.GetPolicy(policyWithVarInExclude) + policy, _, _, err := yamlutils.GetPolicy(policyWithVarInExclude) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -193,7 +193,7 @@ func TestNotAllowedVars_JSONPatchPath(t *testing.T) { } }`) - policy, _, err := yamlutils.GetPolicy(policyWithVarInExclude) + policy, _, _, err := yamlutils.GetPolicy(policyWithVarInExclude) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -238,7 +238,7 @@ func TestNotAllowedVars_JSONPatchPath_ContextRootPositive(t *testing.T) { } }`) - policy, _, err := yamlutils.GetPolicy(policyManifest) + policy, _, _, err := yamlutils.GetPolicy(policyManifest) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -281,7 +281,7 @@ func TestNotAllowedVars_JSONPatchPath_ContextSubPositive(t *testing.T) { } }`) - policy, _, err := yamlutils.GetPolicy(policyManifest) + policy, _, _, err := yamlutils.GetPolicy(policyManifest) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -315,7 +315,7 @@ func TestNotAllowedVars_JSONPatchPath_PositiveCase(t *testing.T) { } }`) - policy, _, err := yamlutils.GetPolicy(policyWithVarInExclude) + policy, _, _, err := yamlutils.GetPolicy(policyWithVarInExclude) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -347,7 +347,7 @@ spec: policyJSON, err := yaml.ToJSON(policyYAML) assert.NilError(t, err) - policy, _, err := yamlutils.GetPolicy(policyJSON) + policy, _, _, err := yamlutils.GetPolicy(policyJSON) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -435,7 +435,7 @@ func TestNotAllowedVars_VariableFormats(t *testing.T) { value: "foo.com" `, tc.input)) - policy, _, err := yamlutils.GetPolicy(policyYAML) + policy, _, _, err := yamlutils.GetPolicy(policyYAML) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false) @@ -481,7 +481,7 @@ spec: policyJSON, err := yaml.ToJSON(policyYAML) assert.NilError(t, err) - policy, _, err := yamlutils.GetPolicy(policyJSON) + policy, _, _, err := yamlutils.GetPolicy(policyJSON) assert.NilError(t, err) err = hasInvalidVariables(policy[0], false)