1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-28 10:28:36 +00:00

feat: Implement PolicyException (#5680)

* feat: Handle Exception

Signed-off-by: Eileen Yu <eileenylj@gmail.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

Signed-off-by: Eileen Yu <eileenylj@gmail.com>
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Eileen 2022-12-16 04:13:14 -05:00 committed by GitHub
parent 85bb5f32be
commit e0f0fdf242
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 114 additions and 11 deletions

View file

@ -17,6 +17,7 @@ package v2alpha1
import (
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
"golang.org/x/exp/slices"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)
@ -43,6 +44,11 @@ func (p *PolicyException) Validate() (errs field.ErrorList) {
return errs
}
// Contains returns true if it contains an exception for the given policy/rule pair
func (p *PolicyException) Contains(policy string, rule string) bool {
return p.Spec.Contains(policy, rule)
}
// PolicyExceptionSpec stores policy exception spec
type PolicyExceptionSpec struct {
// Match defines match clause used to check if a resource applies to the exception
@ -62,6 +68,16 @@ func (p *PolicyExceptionSpec) Validate(path *field.Path) (errs field.ErrorList)
return errs
}
// Contains returns true if it contains an exception for the given policy/rule pair
func (p *PolicyExceptionSpec) Contains(policy string, rule string) bool {
for _, exception := range p.Exceptions {
if exception.Contains(policy, rule) {
return true
}
}
return false
}
// Exception stores infos about a policy and rules
type Exception struct {
// PolicyName identifies the policy to which the exception is applied.
@ -79,6 +95,11 @@ func (p *Exception) Validate(path *field.Path) (errs field.ErrorList) {
return errs
}
// Contains returns true if it contains an exception for the given policy/rule pair
func (p *Exception) Contains(policy string, rule string) bool {
return p.PolicyName == policy && slices.Contains(p.RuleNames, rule)
}
// +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View file

@ -10,6 +10,7 @@ import (
"github.com/kyverno/kyverno/pkg/clients/dclient"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
match "github.com/kyverno/kyverno/pkg/utils/match"
"go.uber.org/multierr"
"k8s.io/apimachinery/pkg/util/sets"
corev1listers "k8s.io/client-go/listers/core/v1"
@ -88,17 +89,17 @@ func (h *handlers) executePolicy(ctx context.Context, logger logr.Logger, policy
nsLabels = ns.GetLabels()
}
// match namespaces
if err := checkNamespace(policy.GetNamespace(), resource); err != nil {
if err := match.CheckNamespace(policy.GetNamespace(), resource); err != nil {
debug.Info("resource namespace didn't match policy namespace", "result", err)
}
// match resource with match/exclude clause
matched := checkMatchesResources(resource, spec.MatchResources, nsLabels)
matched := match.CheckMatchesResources(resource, spec.MatchResources, nsLabels)
if matched != nil {
debug.Info("resource/match didn't match", "result", matched)
continue
}
if spec.ExcludeResources != nil {
excluded := checkMatchesResources(resource, *spec.ExcludeResources, nsLabels)
excluded := match.CheckMatchesResources(resource, *spec.ExcludeResources, nsLabels)
if excluded == nil {
debug.Info("resource/exclude matched")
continue

View file

@ -629,6 +629,7 @@ func main() {
kubeInformer.Rbac().V1().RoleBindings().Lister(),
kubeInformer.Rbac().V1().ClusterRoleBindings().Lister(),
kyvernoInformer.Kyverno().V1beta1().UpdateRequests().Lister().UpdateRequests(config.KyvernoNamespace()),
kyvernoInformer.Kyverno().V2alpha1().PolicyExceptions().Lister(),
urgen,
eventGenerator,
openApiManager,

View file

@ -3,6 +3,8 @@ package engine
import (
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
kyvernov2alpha1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v2alpha1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
enginectx "github.com/kyverno/kyverno/pkg/engine/context"
@ -12,6 +14,8 @@ import (
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// ExcludeFunc is a function used to determine if a resource is excluded
@ -74,6 +78,9 @@ type PolicyContext struct {
APIResource metav1.APIResource
ParentResource metav1.APIResource
}
// peLister list all policy exceptions
peLister kyvernov2alpha1listers.PolicyExceptionLister
}
// Getters
@ -98,6 +105,27 @@ func (c *PolicyContext) JSONContext() enginectx.Interface {
return c.jsonContext
}
func (c *PolicyContext) FindExceptions(rule string) ([]*kyvernov2alpha1.PolicyException, error) {
if c.peLister == nil {
return nil, nil
}
polexs, err := c.peLister.List(labels.Everything())
if err != nil {
return nil, err
}
var result []*kyvernov2alpha1.PolicyException
policyName, err := cache.MetaNamespaceKeyFunc(c.policy)
if err != nil {
return nil, errors.Wrap(err, "failed to compute policy key")
}
for _, polex := range polexs {
if polex.Contains(policyName, rule) {
result = append(result, polex)
}
}
return result, nil
}
// Mutators
func (c *PolicyContext) WithPolicy(policy kyvernov1.PolicyInterface) *PolicyContext {
@ -190,8 +218,13 @@ func (c *PolicyContext) WithSubresourcesInPolicy(subresourcesInPolicy []struct {
return copy
}
// Constructors
func (c *PolicyContext) WithExceptions(peLister kyvernov2alpha1listers.PolicyExceptionLister) *PolicyContext {
copy := c.Copy()
copy.peLister = peLister
return copy
}
// Constructors
func NewPolicyContextWithJsonContext(jsonContext enginectx.Interface) *PolicyContext {
return &PolicyContext{
jsonContext: jsonContext,
@ -212,6 +245,7 @@ func NewPolicyContextFromAdmissionRequest(
configuration config.Configuration,
client dclient.Interface,
informerCacheResolver resolvers.ConfigmapResolver,
peLister kyvernov2alpha1listers.PolicyExceptionLister,
) (*PolicyContext, error) {
ctx, err := newVariablesContext(request, &admissionInfo)
if err != nil {
@ -234,7 +268,8 @@ func NewPolicyContextFromAdmissionRequest(
WithAdmissionOperation(true).
WithInformerCacheResolver(informerCacheResolver).
WithRequestResource(*requestResource).
WithSubresource(request.SubResource)
WithSubresource(request.SubResource).
WithExceptions(peLister)
return policyContext, nil
}

View file

@ -11,6 +11,7 @@ import (
"github.com/go-logr/logr"
gojmespath "github.com/jmespath/go-jmespath"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store"
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/engine/common"
@ -23,6 +24,7 @@ import (
"github.com/kyverno/kyverno/pkg/tracing"
"github.com/kyverno/kyverno/pkg/utils"
"github.com/kyverno/kyverno/pkg/utils/api"
matched "github.com/kyverno/kyverno/pkg/utils/match"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/trace"
appsv1 "k8s.io/api/apps/v1"
@ -31,6 +33,7 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/tools/cache"
)
// Validate applies validation rules from policy on the resource
@ -132,6 +135,23 @@ func validateResource(ctx context.Context, log logr.Logger, rclient registryclie
if !matches(log, rule, enginectx) {
return nil
}
// if matches, check if there is a corresponding policy exception
exception, err := matchesException(enginectx, rule)
// if we found an exception
if err == nil && exception != nil {
key, err := cache.MetaNamespaceKeyFunc(exception)
// TODO: increase metrics
if err != nil {
log.Error(err, "failed to compute policy exception key", "namespace", exception.GetNamespace(), "name", exception.GetName())
} else {
log.V(3).Info("policy rule skipped due to policy exception", "exception", key)
return &response.RuleResponse{
Name: rule.Name,
Message: "Rule skipped because of PolicyException" + key,
Status: response.RuleStatusSkip,
}
}
}
log.V(3).Info("processing validation rule", "matchCount", matchCount, "applyRules", applyRules)
enginectx.jsonContext.Reset()
if hasValidate && !hasYAMLSignatureVerify {
@ -755,3 +775,19 @@ func (v *validator) substituteDeny() error {
v.deny = i.(*kyvernov1.Deny)
return nil
}
// matchesException checks if an exception applies to the resource being admitted
func matchesException(policyContext *PolicyContext, rule *kyvernov1.Rule) (*kyvernov2alpha1.PolicyException, error) {
candidates, err := policyContext.FindExceptions(rule.Name)
if err != nil {
return nil, err
}
for _, candidate := range candidates {
err := matched.CheckMatchesResources(policyContext.newResource, candidate.Spec.Match, policyContext.namespaceLabels)
// if there's no error it means a match
if err == nil {
return candidate, nil
}
}
return nil, nil
}

View file

@ -1,4 +1,4 @@
package cleanup
package match
import (
"fmt"
@ -18,7 +18,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
func checkNamespace(statement string, resource unstructured.Unstructured) error {
func CheckNamespace(statement string, resource unstructured.Unstructured) error {
if statement == "" {
return nil
}
@ -28,7 +28,7 @@ func checkNamespace(statement string, resource unstructured.Unstructured) error
return fmt.Errorf("resource namespace (%s) doesn't match statement (%s)", resource.GetNamespace(), statement)
}
func checkMatchesResources(
func CheckMatchesResources(
resource unstructured.Unstructured,
statement kyvernov2beta1.MatchResources,
namespaceLabels map[string]string,

View file

@ -37,6 +37,7 @@ func NewFakeHandlers(ctx context.Context, policyCache policycache.Cache) webhook
rbLister := informers.Rbac().V1().RoleBindings().Lister()
crbLister := informers.Rbac().V1().ClusterRoleBindings().Lister()
urLister := kyvernoInformers.Kyverno().V1beta1().UpdateRequests().Lister().UpdateRequests(config.KyvernoNamespace())
peLister := kyvernoInformers.Kyverno().V2alpha1().PolicyExceptions().Lister()
return &handlers{
client: dclient,
@ -51,7 +52,7 @@ func NewFakeHandlers(ctx context.Context, policyCache policycache.Cache) webhook
urGenerator: updaterequest.NewFake(),
eventGen: event.NewFake(),
openApiManager: openapi.NewFake(),
pcBuilder: webhookutils.NewPolicyContextBuilder(configuration, dclient, rbLister, crbLister, configMapResolver),
pcBuilder: webhookutils.NewPolicyContextBuilder(configuration, dclient, rbLister, crbLister, configMapResolver, peLister),
urUpdater: webhookutils.NewUpdateRequestUpdater(kyvernoclient, urLister),
}
}

View file

@ -10,6 +10,7 @@ import (
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/client/clientset/versioned"
kyvernov1beta1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v1beta1"
kyvernov2alpha1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v2alpha1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/common"
"github.com/kyverno/kyverno/pkg/config"
@ -53,6 +54,7 @@ type handlers struct {
rbLister rbacv1listers.RoleBindingLister
crbLister rbacv1listers.ClusterRoleBindingLister
urLister kyvernov1beta1listers.UpdateRequestNamespaceLister
peLister kyvernov2alpha1listers.PolicyExceptionLister
urGenerator webhookgenerate.Generator
eventGen event.Interface
@ -75,6 +77,7 @@ func NewHandlers(
rbLister rbacv1listers.RoleBindingLister,
crbLister rbacv1listers.ClusterRoleBindingLister,
urLister kyvernov1beta1listers.UpdateRequestNamespaceLister,
peLister kyvernov2alpha1listers.PolicyExceptionLister,
urGenerator webhookgenerate.Generator,
eventGen event.Interface,
openApiManager openapi.ValidateInterface,
@ -91,10 +94,11 @@ func NewHandlers(
rbLister: rbLister,
crbLister: crbLister,
urLister: urLister,
peLister: peLister,
urGenerator: urGenerator,
eventGen: eventGen,
openApiManager: openApiManager,
pcBuilder: webhookutils.NewPolicyContextBuilder(configuration, client, rbLister, crbLister, informerCacheResolvers),
pcBuilder: webhookutils.NewPolicyContextBuilder(configuration, client, rbLister, crbLister, informerCacheResolvers, peLister),
urUpdater: webhookutils.NewUpdateRequestUpdater(kyvernoClient, urLister),
admissionReports: admissionReports,
}

View file

@ -2,6 +2,7 @@ package utils
import (
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
kyvernov2alpha1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v2alpha1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
"github.com/kyverno/kyverno/pkg/engine"
@ -22,6 +23,7 @@ type policyContextBuilder struct {
rbLister rbacv1listers.RoleBindingLister
crbLister rbacv1listers.ClusterRoleBindingLister
informerCacheResolvers resolvers.ConfigmapResolver
peLister kyvernov2alpha1listers.PolicyExceptionLister
}
func NewPolicyContextBuilder(
@ -30,6 +32,7 @@ func NewPolicyContextBuilder(
rbLister rbacv1listers.RoleBindingLister,
crbLister rbacv1listers.ClusterRoleBindingLister,
informerCacheResolvers resolvers.ConfigmapResolver,
peLister kyvernov2alpha1listers.PolicyExceptionLister,
) PolicyContextBuilder {
return &policyContextBuilder{
configuration: configuration,
@ -37,6 +40,7 @@ func NewPolicyContextBuilder(
rbLister: rbLister,
crbLister: crbLister,
informerCacheResolvers: informerCacheResolvers,
peLister: peLister,
}
}
@ -50,5 +54,5 @@ func (b *policyContextBuilder) Build(request *admissionv1.AdmissionRequest) (*en
userRequestInfo.Roles = roles
userRequestInfo.ClusterRoles = clusterRoles
}
return engine.NewPolicyContextFromAdmissionRequest(request, userRequestInfo, b.configuration, b.client, b.informerCacheResolvers)
return engine.NewPolicyContextFromAdmissionRequest(request, userRequestInfo, b.configuration, b.client, b.informerCacheResolvers, b.peLister)
}