Skip to content

Commit

Permalink
Add warn enforcement action (#1107)
Browse files Browse the repository at this point in the history
* add warn enforcement action

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

* fix enforcementAction

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

* update

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

* address comments

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

* add const

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

* update messages

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

* add test label

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
  • Loading branch information
sozercan committed Mar 9, 2021
1 parent 84f64c4 commit 426949f
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 72 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ KUSTOMIZE_VERSION ?= 3.8.8
BATS_VERSION ?= 1.2.1
KUBECTL_KUSTOMIZE_VERSION ?= 1.20.1-${KUSTOMIZE_VERSION}
HELM_VERSION ?= 2.17.0
HELM_ARGS ?=
HELM_ARGS ?=
GATEKEEPER_NAMESPACE ?= gatekeeper-system

BUILD_COMMIT := $(shell ./build/get-build-commit.sh)
Expand Down
2 changes: 1 addition & 1 deletion pkg/target/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func TestValidateConstraint(t *testing.T) {
],
"namespaceSelector": {
"matchExpressions": [{
"key": "someKey",
"key": "someKey",
"operator": "Blah",
"values": ["some value"]
}]
Expand Down
7 changes: 4 additions & 3 deletions pkg/util/enforcement_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ type EnforcementAction string
const (
Deny EnforcementAction = "deny"
Dryrun EnforcementAction = "dryrun"
Warn EnforcementAction = "warn"
Unrecognized EnforcementAction = "unrecognized"
)

var supportedEnforcementActions = []EnforcementAction{Deny, Dryrun}
var KnownEnforcementActions = []EnforcementAction{Deny, Dryrun, Unrecognized}
var supportedEnforcementActions = []EnforcementAction{Deny, Dryrun, Warn}
var KnownEnforcementActions = []EnforcementAction{Deny, Dryrun, Warn, Unrecognized}

func ValidateEnforcementAction(input EnforcementAction) error {
for _, n := range supportedEnforcementActions {
if input == n {
return nil
}
}
return fmt.Errorf("Could not find the provided enforcementAction value within the supported list %v", supportedEnforcementActions)
return fmt.Errorf("could not find the provided enforcementAction value within the supported list %v", supportedEnforcementActions)
}

func GetEnforcementAction(item map[string]interface{}) (EnforcementAction, error) {
Expand Down
147 changes: 94 additions & 53 deletions pkg/webhook/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// httpStatusWarning is the HTTP return code for displaying warning messages in admission webhook (supported in Kubernetes v1.19+)
// https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response
const httpStatusWarning = 299

var (
maxServingThreads = flag.Int("max-serving-threads", -1, "(alpha) cap the number of threads handling non-trivial requests, -1 means an infinite number of threads")
)
Expand Down Expand Up @@ -164,7 +168,6 @@ func (h *validationHandler) Handle(ctx context.Context, req admission.Request) a
}

if isExcludedNamespace {

requestResponse = allowResponse
return admission.ValidationResponse(true, "Namespace is set to be ignored by Gatekeeper config")
}
Expand All @@ -182,23 +185,35 @@ func (h *validationHandler) Handle(ctx context.Context, req admission.Request) a
}

res := resp.Results()
msgs := h.getDenyMessages(res, req)
if len(msgs) > 0 {
vResp := admission.ValidationResponse(false, strings.Join(msgs, "\n"))
denyMsgs, warnMsgs := h.getValidationMessages(res, req)

if len(denyMsgs) > 0 {
vResp := admission.ValidationResponse(false, strings.Join(denyMsgs, "\n"))
if vResp.Result == nil {
vResp.Result = &metav1.Status{}
}
if len(warnMsgs) > 0 {
vResp.Warnings = warnMsgs
}
vResp.Result.Code = http.StatusForbidden
requestResponse = denyResponse
return vResp
}

requestResponse = allowResponse
return admission.ValidationResponse(true, "")
vResp := admission.ValidationResponse(true, "")
if vResp.Result == nil {
vResp.Result = &metav1.Status{}
}
if len(warnMsgs) > 0 {
vResp.Warnings = warnMsgs
vResp.Result.Code = httpStatusWarning
}
return vResp
}

func (h *validationHandler) getDenyMessages(res []*rtypes.Result, req admission.Request) []string {
var msgs []string
func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req admission.Request) ([]string, []string) {
var denyMsgs, warnMsgs []string
var resourceName string
if len(res) > 0 && (*logDenies || *emitAdmissionEvents) {
resourceName = req.AdmissionRequest.Name
Expand All @@ -212,57 +227,83 @@ func (h *validationHandler) getDenyMessages(res []*rtypes.Result, req admission.
}
}
for _, r := range res {
if r.EnforcementAction == "deny" || r.EnforcementAction == "dryrun" {
if *logDenies {
log.WithValues(
logging.Process, "admission",
logging.EventType, "violation",
logging.ConstraintName, r.Constraint.GetName(),
logging.ConstraintGroup, r.Constraint.GroupVersionKind().Group,
logging.ConstraintAPIVersion, r.Constraint.GroupVersionKind().Version,
logging.ConstraintKind, r.Constraint.GetKind(),
logging.ConstraintAction, r.EnforcementAction,
logging.ResourceGroup, req.AdmissionRequest.Kind.Group,
logging.ResourceAPIVersion, req.AdmissionRequest.Kind.Version,
logging.ResourceKind, req.AdmissionRequest.Kind.Kind,
logging.ResourceNamespace, req.AdmissionRequest.Namespace,
logging.ResourceName, resourceName,
logging.RequestUsername, req.AdmissionRequest.UserInfo.Username,
).Info("denied admission")
if err := util.ValidateEnforcementAction(util.EnforcementAction(r.EnforcementAction)); err != nil {
continue
}
if *logDenies {
log.WithValues(
logging.Process, "admission",
logging.EventType, "violation",
logging.ConstraintName, r.Constraint.GetName(),
logging.ConstraintGroup, r.Constraint.GroupVersionKind().Group,
logging.ConstraintAPIVersion, r.Constraint.GroupVersionKind().Version,
logging.ConstraintKind, r.Constraint.GetKind(),
logging.ConstraintAction, r.EnforcementAction,
logging.ResourceGroup, req.AdmissionRequest.Kind.Group,
logging.ResourceAPIVersion, req.AdmissionRequest.Kind.Version,
logging.ResourceKind, req.AdmissionRequest.Kind.Kind,
logging.ResourceNamespace, req.AdmissionRequest.Namespace,
logging.ResourceName, resourceName,
logging.RequestUsername, req.AdmissionRequest.UserInfo.Username,
).Info("denied admission")
}
if *emitAdmissionEvents {
annotations := map[string]string{
logging.Process: "admission",
logging.EventType: "violation",
logging.ConstraintName: r.Constraint.GetName(),
logging.ConstraintGroup: r.Constraint.GroupVersionKind().Group,
logging.ConstraintAPIVersion: r.Constraint.GroupVersionKind().Version,
logging.ConstraintKind: r.Constraint.GetKind(),
logging.ConstraintAction: r.EnforcementAction,
logging.ResourceGroup: req.AdmissionRequest.Kind.Group,
logging.ResourceAPIVersion: req.AdmissionRequest.Kind.Version,
logging.ResourceKind: req.AdmissionRequest.Kind.Kind,
logging.ResourceNamespace: req.AdmissionRequest.Namespace,
logging.ResourceName: resourceName,
logging.RequestUsername: req.AdmissionRequest.UserInfo.Username,
}
if *emitAdmissionEvents {
annotations := map[string]string{
logging.Process: "admission",
logging.EventType: "violation",
logging.ConstraintName: r.Constraint.GetName(),
logging.ConstraintGroup: r.Constraint.GroupVersionKind().Group,
logging.ConstraintAPIVersion: r.Constraint.GroupVersionKind().Version,
logging.ConstraintKind: r.Constraint.GetKind(),
logging.ConstraintAction: r.EnforcementAction,
logging.ResourceGroup: req.AdmissionRequest.Kind.Group,
logging.ResourceAPIVersion: req.AdmissionRequest.Kind.Version,
logging.ResourceKind: req.AdmissionRequest.Kind.Kind,
logging.ResourceNamespace: req.AdmissionRequest.Namespace,
logging.ResourceName: resourceName,
logging.RequestUsername: req.AdmissionRequest.UserInfo.Username,
}
eventMsg := "Admission webhook \"validation.gatekeeper.sh\" denied request"
reason := "FailedAdmission"
if r.EnforcementAction == "dryrun" {
eventMsg = "Dryrun violation"
reason = "DryrunViolation"
}
ref := getViolationRef(h.gkNamespace, req.AdmissionRequest.Kind.Kind, resourceName, req.AdmissionRequest.Namespace, r.Constraint.GetKind(), r.Constraint.GetName(), r.Constraint.GetNamespace())
h.eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "%s, Resource Namespace: %s, Constraint: %s, Message: %s", eventMsg, req.AdmissionRequest.Namespace, r.Constraint.GetName(), r.Msg)
var eventMsg, reason string
switch r.EnforcementAction {
case string(util.Dryrun):
eventMsg = "Dryrun violation"
reason = "DryrunViolation"
case string(util.Warn):
eventMsg = "Admission webhook \"validation.gatekeeper.sh\" raised a warning for this request"
reason = "WarningAdmission"
default:
eventMsg = "Admission webhook \"validation.gatekeeper.sh\" denied request"
reason = "FailedAdmission"
}
ref := getViolationRef(
h.gkNamespace,
req.AdmissionRequest.Kind.Kind,
resourceName,
req.AdmissionRequest.Namespace,
r.Constraint.GetKind(),
r.Constraint.GetName(),
r.Constraint.GetNamespace())
h.eventRecorder.AnnotatedEventf(
ref,
annotations,
corev1.EventTypeWarning,
reason,
"%s, Resource Namespace: %s, Constraint: %s, Message: %s",
eventMsg,
req.AdmissionRequest.Namespace,
r.Constraint.GetName(),
r.Msg)
}

if r.EnforcementAction == string(util.Deny) {
denyMsgs = append(denyMsgs, fmt.Sprintf("[%s] %s", r.Constraint.GetName(), r.Msg))
}
// only deny enforcementAction should prompt deny admission response
if r.EnforcementAction == "deny" {
msgs = append(msgs, fmt.Sprintf("[denied by %s] %s", r.Constraint.GetName(), r.Msg))

if r.EnforcementAction == string(util.Warn) {
warnMsgs = append(warnMsgs, fmt.Sprintf("[%s] %s", r.Constraint.GetName(), r.Msg))
}
}
return msgs
return denyMsgs, warnMsgs
}

// validateGatekeeperResources returns whether an issue is user error (vs internal) and any errors
Expand Down Expand Up @@ -332,7 +373,7 @@ func (h *validationHandler) validateConstraint(ctx context.Context, req admissio

func (h *validationHandler) validateConfigResource(ctx context.Context, req admission.Request) error {
if req.Name != keys.Config.Name {
return fmt.Errorf("Config resource must have name 'config'")
return fmt.Errorf("config resource must have name 'config'")
}
return nil
}
Expand Down
68 changes: 55 additions & 13 deletions pkg/webhook/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ func newConstraint(kind, name string, enforcementAction string, t *testing.T) *u
return c
}

func TestGetDenyMessages(t *testing.T) {
func TestGetValidationMessages(t *testing.T) {
resDryRun := &rtypes.Result{
Msg: "test",
Constraint: newConstraint("Foo", "ph", "dryrun", t),
Expand All @@ -573,61 +573,100 @@ func TestGetDenyMessages(t *testing.T) {
Constraint: newConstraint("Foo", "ph", "deny", t),
EnforcementAction: "deny",
}
resWarn := &rtypes.Result{
Msg: "test",
Constraint: newConstraint("Foo", "ph", "warn", t),
EnforcementAction: "warn",
}
resRandom := &rtypes.Result{
Msg: "test",
Constraint: newConstraint("Foo", "ph", "random", t),
EnforcementAction: "random",
}

tc := []struct {
Name string
Result []*rtypes.Result
ExpectedMsgCount int
Name string
Result []*rtypes.Result
ExpectedDenyMsgCount int
ExpectedWarnMsgCount int
}{
{
Name: "Only One Dry Run",
Result: []*rtypes.Result{
resDryRun,
},
ExpectedMsgCount: 0,
ExpectedDenyMsgCount: 0,
ExpectedWarnMsgCount: 0,
},
{
Name: "Only One Deny",
Result: []*rtypes.Result{
resDeny,
},
ExpectedMsgCount: 1,
ExpectedDenyMsgCount: 1,
ExpectedWarnMsgCount: 0,
},
{
Name: "Only One Warn",
Result: []*rtypes.Result{
resWarn,
},
ExpectedDenyMsgCount: 0,
ExpectedWarnMsgCount: 1,
},
{
Name: "One Dry Run and One Deny",
Result: []*rtypes.Result{
resDryRun,
resDeny,
},
ExpectedMsgCount: 1,
ExpectedDenyMsgCount: 1,
ExpectedWarnMsgCount: 0,
},
{
Name: "One Dry Run, One Deny, One Warn",
Result: []*rtypes.Result{
resDryRun,
resDeny,
resWarn,
},
ExpectedDenyMsgCount: 1,
ExpectedWarnMsgCount: 1,
},
{
Name: "Two Deny",
Result: []*rtypes.Result{
resDeny,
resDeny,
},
ExpectedMsgCount: 2,
ExpectedDenyMsgCount: 2,
ExpectedWarnMsgCount: 0,
},
{
Name: "Two Warn",
Result: []*rtypes.Result{
resWarn,
resWarn,
},
ExpectedDenyMsgCount: 0,
ExpectedWarnMsgCount: 2,
},
{
Name: "Two Dry Run",
Result: []*rtypes.Result{
resDryRun,
resDryRun,
},
ExpectedMsgCount: 0,
ExpectedDenyMsgCount: 0,
ExpectedWarnMsgCount: 0,
},
{
Name: "Random EnforcementAction",
Result: []*rtypes.Result{
resRandom,
},
ExpectedMsgCount: 0,
ExpectedDenyMsgCount: 0,
ExpectedWarnMsgCount: 0,
},
}

Expand All @@ -654,9 +693,12 @@ func TestGetDenyMessages(t *testing.T) {
},
},
}
msgs := handler.getDenyMessages(tt.Result, review)
if len(msgs) != tt.ExpectedMsgCount {
t.Errorf("expected count = %d; actual count = %d", tt.ExpectedMsgCount, len(msgs))
denyMsgs, warnMsgs := handler.getValidationMessages(tt.Result, review)
if len(denyMsgs) != tt.ExpectedDenyMsgCount {
t.Errorf("denyMsgs: expected count = %d; actual count = %d", tt.ExpectedDenyMsgCount, len(denyMsgs))
}
if len(warnMsgs) != tt.ExpectedWarnMsgCount {
t.Errorf("warnMsgs: expected count = %d; actual count = %d", tt.ExpectedWarnMsgCount, len(warnMsgs))
}
}
t.Run(tt.Name, testFn)
Expand Down

0 comments on commit 426949f

Please sign in to comment.