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:
parent
85bb5f32be
commit
e0f0fdf242
9 changed files with 114 additions and 11 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue