diff --git a/definitions/install.yaml b/definitions/install.yaml index a4f2e544f8..1d01a8cb8a 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -38,7 +38,6 @@ spec: type: object required: - name - - match properties: name: type: string @@ -177,6 +176,19 @@ spec: - key # can be of any type - operator # typed - value # can be of any type + deny: + type: object + properties: + message: + type: string + conditions: + type: array + items: + type: object + required: + - key # can be of any type + - operator # typed + - value # can be of any type mutate: type: object properties: diff --git a/definitions/install_debug.yaml b/definitions/install_debug.yaml index e9a1343482..7c8812163e 100644 --- a/definitions/install_debug.yaml +++ b/definitions/install_debug.yaml @@ -38,7 +38,6 @@ spec: type: object required: - name - - match properties: name: type: string @@ -177,6 +176,19 @@ spec: - key # can be of any type - operator # typed - value # can be of any type + deny: + type: object + properties: + message: + type: string + conditions: + type: array + items: + type: object + required: + - key # can be of any type + - operator # typed + - value # can be of any type mutate: type: object properties: diff --git a/pkg/api/kyverno/v1/types.go b/pkg/api/kyverno/v1/types.go index 29c44b4385..c4f57cce38 100644 --- a/pkg/api/kyverno/v1/types.go +++ b/pkg/api/kyverno/v1/types.go @@ -141,6 +141,12 @@ type Rule struct { Mutation Mutation `json:"mutate,omitempty"` Validation Validation `json:"validate,omitempty"` Generation Generation `json:"generate,omitempty"` + Deny *Deny `json:"deny,omitempty"` +} + +type Deny struct { + Message string `json:"message,omitempty"` + Conditions []Condition `json:"conditions,omitempty"` } //Condition defines the evaluation condition diff --git a/pkg/api/kyverno/v1/utils.go b/pkg/api/kyverno/v1/utils.go index abe266eabd..f08b91ee5c 100644 --- a/pkg/api/kyverno/v1/utils.go +++ b/pkg/api/kyverno/v1/utils.go @@ -27,6 +27,10 @@ func (r Rule) HasGenerate() bool { return !reflect.DeepEqual(r.Generation, Generation{}) } +func (r Rule) HasDeny() bool { + return r.Deny != nil +} + // DeepCopyInto is declared because k8s:deepcopy-gen is // not able to generate this method for interface{} member func (in *Mutation) DeepCopyInto(out *Mutation) { diff --git a/pkg/engine/deny.go b/pkg/engine/deny.go new file mode 100644 index 0000000000..0179f8a3f6 --- /dev/null +++ b/pkg/engine/deny.go @@ -0,0 +1,26 @@ +package engine + +import ( + "fmt" + + "github.com/go-logr/logr" + "github.com/nirmata/kyverno/pkg/engine/context" + + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/nirmata/kyverno/pkg/engine/variables" +) + +func Deny(logger logr.Logger, policy v1.ClusterPolicy, ctx *context.Context) error { + for _, rule := range policy.Spec.Rules { + if rule.Deny != nil { + sliceCopy := make([]v1.Condition, len(rule.Deny.Conditions)) + copy(sliceCopy, rule.Deny.Conditions) + + if !variables.EvaluateConditions(logger, ctx, sliceCopy) { + return fmt.Errorf("request has been denied by policy %s due to - %s", policy.Name, rule.Deny.Message) + } + } + } + + return nil +} diff --git a/pkg/policy/validate.go b/pkg/policy/validate.go index 96efd07cce..e835fde5d9 100644 --- a/pkg/policy/validate.go +++ b/pkg/policy/validate.go @@ -51,12 +51,6 @@ func Validate(policyRaw []byte, client *dclient.Client, mock bool, openAPIContro if path, err := validateResources(rule); err != nil { return fmt.Errorf("path: spec.rules[%d].%s: %v", i, path, err) } - // validate rule types - // only one type of rule is allowed per rule - if err := validateRuleType(rule); err != nil { - // as there are more than 1 operation in rule, not need to evaluate it further - return fmt.Errorf("path: spec.rules[%d]: %v", i, err) - } // validate rule actions // - Mutate // - Validate @@ -122,6 +116,10 @@ func ruleOnlyDealsWithResourceMetaData(rule kyverno.Rule) bool { } func validateResources(rule kyverno.Rule) (string, error) { + if rule.HasDeny() { + return "", nil + } + // validate userInfo in match and exclude if path, err := validateUserInfo(rule); err != nil { return fmt.Sprintf("resources.%s", path), err @@ -153,7 +151,7 @@ func validateUniqueRuleName(p kyverno.ClusterPolicy) (string, error) { // validateRuleType checks only one type of rule is defined per rule func validateRuleType(r kyverno.Rule) error { - ruleTypes := []bool{r.HasMutate(), r.HasValidate(), r.HasGenerate()} + ruleTypes := []bool{r.HasMutate(), r.HasValidate(), r.HasGenerate(), r.HasDeny()} operationCount := func() int { count := 0 diff --git a/pkg/webhooks/checker.go b/pkg/webhooks/checker.go index 4a52fa7649..a648a0034c 100644 --- a/pkg/webhooks/checker.go +++ b/pkg/webhooks/checker.go @@ -4,7 +4,7 @@ import ( "k8s.io/api/admission/v1beta1" ) -func (ws *WebhookServer) handleVerifyRequest(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { +func (ws *WebhookServer) verifyHandler(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { logger := ws.log.WithValues("action", "verify", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) logger.V(4).Info("incoming request") return &v1beta1.AdmissionResponse{ diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index 7f0fc4a692..912349ebf8 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -16,7 +16,7 @@ import ( ) //HandleGenerate handles admission-requests for policies with generate rules -func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, policies []kyverno.ClusterPolicy, patchedResource []byte, roles, clusterRoles []string) (bool, string) { +func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, policies []kyverno.ClusterPolicy, ctx *context.Context, userRequestInfo kyverno.RequestInfo) (bool, string) { logger := ws.log.WithValues("action", "generation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) logger.V(4).Info("incoming request") var engineResponses []response.EngineResponse @@ -31,27 +31,6 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic // CREATE resources, do not have name, assigned in admission-request - userRequestInfo := kyverno.RequestInfo{ - Roles: roles, - ClusterRoles: clusterRoles, - AdmissionUserInfo: request.UserInfo} - // build context - ctx := context.NewContext() - // load incoming resource into the context - err = ctx.AddResource(request.Object.Raw) - if err != nil { - logger.Error(err, "failed to load incoming resource in context") - } - err = ctx.AddUserInfo(userRequestInfo) - if err != nil { - logger.Error(err, "failed to load userInfo in context") - } - // load service account in context - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) - if err != nil { - logger.Error(err, "failed to load service account in context") - } - policyContext := engine.PolicyContext{ NewResource: *resource, AdmissionInfo: userRequestInfo, diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index 0afdefa90a..2f1cf8af51 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -18,36 +18,12 @@ import ( // HandleMutation handles mutating webhook admission request // return value: generated patches -func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resource unstructured.Unstructured, policies []kyverno.ClusterPolicy, roles, clusterRoles []string) []byte { +func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resource unstructured.Unstructured, policies []kyverno.ClusterPolicy, ctx *context.Context, userRequestInfo kyverno.RequestInfo) []byte { logger := ws.log.WithValues("action", "mutation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) logger.V(4).Info("incoming request") var patches [][]byte var engineResponses []response.EngineResponse - - userRequestInfo := kyverno.RequestInfo{ - Roles: roles, - ClusterRoles: clusterRoles, - AdmissionUserInfo: request.UserInfo} - - // build context - ctx := context.NewContext() - var err error - // load incoming resource into the context - err = ctx.AddResource(request.Object.Raw) - if err != nil { - logger.Error(err, "failed to load incoming resource in context") - } - - err = ctx.AddUserInfo(userRequestInfo) - if err != nil { - logger.Error(err, "failed to load userInfo in context") - } - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) - if err != nil { - logger.Error(err, "failed to load service account in context") - } - policyContext := engine.PolicyContext{ NewResource: resource, AdmissionInfo: userRequestInfo, diff --git a/pkg/webhooks/policymutation.go b/pkg/webhooks/policymutation.go index dea2f87fff..e9535a735d 100644 --- a/pkg/webhooks/policymutation.go +++ b/pkg/webhooks/policymutation.go @@ -16,7 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (ws *WebhookServer) handlePolicyMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { +func (ws *WebhookServer) policyMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { logger := ws.log.WithValues("action", "policymutation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) var policy *kyverno.ClusterPolicy raw := request.Object.Raw diff --git a/pkg/webhooks/policyvalidation.go b/pkg/webhooks/policyvalidation.go index 2923037c98..5b2cf94c18 100644 --- a/pkg/webhooks/policyvalidation.go +++ b/pkg/webhooks/policyvalidation.go @@ -9,7 +9,7 @@ import ( ) //HandlePolicyValidation performs the validation check on policy resource -func (ws *WebhookServer) handlePolicyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { +func (ws *WebhookServer) policyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { //TODO: can this happen? wont this be picked by OpenAPI spec schema ? if err := policyvalidate.Validate(request.Object.Raw, ws.client, false, ws.openAPIController); err != nil { return &v1beta1.AdmissionResponse{ diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 83d1924931..bef9e6d6a5 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -10,6 +10,11 @@ import ( "net/http" "time" + v1 "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + context2 "github.com/nirmata/kyverno/pkg/engine/context" + + "github.com/nirmata/kyverno/pkg/engine" + "github.com/nirmata/kyverno/pkg/openapi" "github.com/go-logr/logr" @@ -131,11 +136,11 @@ func NewWebhookServer( openAPIController: openAPIController, } mux := http.NewServeMux() - mux.HandleFunc(config.MutatingWebhookServicePath, ws.handlerFunc(ws.handleMutateAdmissionRequest, true)) - mux.HandleFunc(config.ValidatingWebhookServicePath, ws.handlerFunc(ws.handleValidateAdmissionRequest, true)) - mux.HandleFunc(config.PolicyMutatingWebhookServicePath, ws.handlerFunc(ws.handlePolicyMutation, true)) - mux.HandleFunc(config.PolicyValidatingWebhookServicePath, ws.handlerFunc(ws.handlePolicyValidation, true)) - mux.HandleFunc(config.VerifyMutatingWebhookServicePath, ws.handlerFunc(ws.handleVerifyRequest, false)) + mux.HandleFunc(config.MutatingWebhookServicePath, ws.handlerFunc(ws.resourceMutation, true)) + mux.HandleFunc(config.ValidatingWebhookServicePath, ws.handlerFunc(ws.resourceValidation, true)) + mux.HandleFunc(config.PolicyMutatingWebhookServicePath, ws.handlerFunc(ws.policyMutation, true)) + mux.HandleFunc(config.PolicyValidatingWebhookServicePath, ws.handlerFunc(ws.policyValidation, true)) + mux.HandleFunc(config.VerifyMutatingWebhookServicePath, ws.handlerFunc(ws.verifyHandler, false)) ws.server = http.Server{ Addr: ":443", // Listen on port for HTTPS requests TLSConfig: &tlsConfig, @@ -190,7 +195,7 @@ func (ws *WebhookServer) handlerFunc(handler func(request *v1beta1.AdmissionRequ } } -func (ws *WebhookServer) handleMutateAdmissionRequest(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { +func (ws *WebhookServer) resourceMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { logger := ws.log.WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) policies, err := ws.pMetaStore.ListAll() if err != nil { @@ -199,9 +204,8 @@ func (ws *WebhookServer) handleMutateAdmissionRequest(request *v1beta1.Admission return &v1beta1.AdmissionResponse{Allowed: true} } - var roles, clusterRoles []string - // getRoleRef only if policy has roles/clusterroles defined + var roles, clusterRoles []string if containRBACinfo(policies) { roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request) if err != nil { @@ -233,17 +237,51 @@ func (ws *WebhookServer) handleMutateAdmissionRequest(request *v1beta1.Admission } } + userRequestInfo := v1.RequestInfo{ + Roles: roles, + ClusterRoles: clusterRoles, + AdmissionUserInfo: request.UserInfo} + + // build context + ctx := context2.NewContext() + // load incoming resource into the context + err = ctx.AddResource(request.Object.Raw) + if err != nil { + logger.Error(err, "failed to load incoming resource in context") + } + + err = ctx.AddUserInfo(userRequestInfo) + if err != nil { + logger.Error(err, "failed to load userInfo in context") + } + err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + if err != nil { + logger.Error(err, "failed to load service account in context") + } + + for _, policy := range policies { + if err := engine.Deny(logger, policy, ctx); err != nil { + return &v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Status: "Failure", + Message: err.Error(), + }, + } + } + } + // MUTATION // mutation failure should not block the resource creation // any mutation failure is reported as the violation - patches := ws.HandleMutation(request, resource, policies, roles, clusterRoles) + patches := ws.HandleMutation(request, resource, policies, ctx, userRequestInfo) // patch the resource with patches before handling validation rules patchedResource := processResourceWithPatches(patches, request.Object.Raw, logger) if ws.resourceWebhookWatcher != nil && ws.resourceWebhookWatcher.RunValidationInMutatingWebhook == "true" { // VALIDATION - ok, msg := ws.HandleValidation(request, policies, patchedResource, roles, clusterRoles) + ok, msg := ws.HandleValidation(request, policies, patchedResource, ctx, userRequestInfo) if !ok { logger.Info("admission request denied") return &v1beta1.AdmissionResponse{ @@ -261,7 +299,7 @@ func (ws *WebhookServer) handleMutateAdmissionRequest(request *v1beta1.Admission // Success -> Generate Request CR created successsfully // Failed -> Failed to create Generate Request CR if request.Operation == v1beta1.Create { - ok, msg := ws.HandleGenerate(request, policies, patchedResource, roles, clusterRoles) + ok, msg := ws.HandleGenerate(request, policies, ctx, userRequestInfo) if !ok { logger.Info("admission request denied") return &v1beta1.AdmissionResponse{ @@ -285,7 +323,7 @@ func (ws *WebhookServer) handleMutateAdmissionRequest(request *v1beta1.Admission } } -func (ws *WebhookServer) handleValidateAdmissionRequest(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { +func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse { logger := ws.log.WithValues("uid", request.UID, "kind", request.Kind.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) policies, err := ws.pMetaStore.ListAll() if err != nil { @@ -305,8 +343,30 @@ func (ws *WebhookServer) handleValidateAdmissionRequest(request *v1beta1.Admissi } } + userRequestInfo := v1.RequestInfo{ + Roles: roles, + ClusterRoles: clusterRoles, + AdmissionUserInfo: request.UserInfo} + + // build context + ctx := context2.NewContext() + // load incoming resource into the context + err = ctx.AddResource(request.Object.Raw) + if err != nil { + logger.Error(err, "failed to load incoming resource in context") + } + + err = ctx.AddUserInfo(userRequestInfo) + if err != nil { + logger.Error(err, "failed to load userInfo in context") + } + err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) + if err != nil { + logger.Error(err, "failed to load service account in context") + } + // VALIDATION - ok, msg := ws.HandleValidation(request, policies, nil, roles, clusterRoles) + ok, msg := ws.HandleValidation(request, policies, nil, ctx, userRequestInfo) if !ok { logger.Info("admission request denied") return &v1beta1.AdmissionResponse{ diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index d0b49347ad..e0d564e942 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -17,7 +17,7 @@ import ( // HandleValidation handles validating webhook admission request // If there are no errors in validating rule we apply generation rules // patchedResource is the (resource + patches) after applying mutation rules -func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, policies []kyverno.ClusterPolicy, patchedResource []byte, roles, clusterRoles []string) (bool, string) { +func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, policies []kyverno.ClusterPolicy, patchedResource []byte, ctx *context.Context, userRequestInfo kyverno.RequestInfo) (bool, string) { logger := ws.log.WithValues("action", "validation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) logger.V(4).Info("incoming request") @@ -28,27 +28,6 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol logger.Error(err, "failed to extract resource") return true, "" } - userRequestInfo := kyverno.RequestInfo{ - Roles: roles, - ClusterRoles: clusterRoles, - AdmissionUserInfo: request.UserInfo} - // build context - ctx := context.NewContext() - // load incoming resource into the context - err = ctx.AddResource(request.Object.Raw) - if err != nil { - logger.Error(err, "failed to load incoming resource in context") - } - - err = ctx.AddUserInfo(userRequestInfo) - if err != nil { - logger.Error(err, "failed to load userInfo in context") - } - - err = ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) - if err != nil { - logger.Error(err, "failed to load service account in context") - } policyContext := engine.PolicyContext{ NewResource: newR,