-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: adding scopedenforcementactions #403
Changes from 25 commits
4aabc45
81b2d08
9deaab2
909437e
e837a1e
a6f1641
46a172b
7a2ff49
e054366
9bbfbc2
076751a
8f28a01
1c6b9d8
ae81539
5af2f0c
a654a76
3cb703c
03dd160
5080e95
d996311
759157f
c9ba827
7cbf00d
86bae52
2428b21
6456bd6
7fefe2d
9fdd218
bbefecf
ac65fcf
d87f02f
675353b
ce5a973
67de36b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,10 +3,21 @@ package constraints | |
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
) | ||
|
||
type ScopedEnforcementAction struct { | ||
Action string `json:"action"` | ||
EnforcementPoints []EnforcementPoint `json:"enforcementPoints"` | ||
} | ||
|
||
type EnforcementPoint struct { | ||
Name string `json:"name"` | ||
} | ||
|
||
const ( | ||
// Group is the API Group of Constraints. | ||
Group = "constraints.gatekeeper.sh" | ||
|
@@ -17,6 +28,11 @@ const ( | |
// | ||
// This is the default EnforcementAction. | ||
EnforcementActionDeny = "deny" | ||
|
||
EnforcementActionScoped = "scoped" | ||
|
||
// AllEnforcementPoints is a wildcard to indicate all enforcement points. | ||
AllEnforcementPoints = "*" | ||
) | ||
|
||
var ( | ||
|
@@ -26,6 +42,9 @@ var ( | |
|
||
// ErrSchema is a specific error that a Constraint failed schema validation. | ||
ErrSchema = errors.New("schema validation failed") | ||
|
||
// ErrMissingRequiredField is a specific error that a field is missing from a Constraint. | ||
ErrMissingRequiredField = errors.New("missing required field") | ||
) | ||
|
||
// GetEnforcementAction returns a Constraint's enforcementAction, which indicates | ||
|
@@ -45,3 +64,101 @@ func GetEnforcementAction(constraint *unstructured.Unstructured) (string, error) | |
|
||
return action, nil | ||
} | ||
|
||
func IsEnforcementActionScoped(action string) bool { | ||
return strings.EqualFold(action, EnforcementActionScoped) | ||
} | ||
|
||
// GetEnforcementActionsForEP returns a map of enforcement actions for enforcement points passed in. | ||
func GetEnforcementActionsForEP(constraint *unstructured.Unstructured, eps []string) (map[string]map[string]bool, error) { | ||
if len(eps) == 0 { | ||
return nil, fmt.Errorf("enforcement points must be provided to get enforcement actions") | ||
} | ||
|
||
scopedActions, found, err := getNestedFieldAsArray(constraint.Object, "spec", "scopedEnforcementActions") | ||
JaydipGabani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
return nil, fmt.Errorf("%w: invalid spec.enforcementActionPerEP", ErrInvalidConstraint) | ||
} | ||
if !found { | ||
return nil, fmt.Errorf("%w: spec.scopedEnforcementAction must be defined", ErrMissingRequiredField) | ||
} | ||
|
||
scopedEnforcementActions, err := convertToSliceScopedEnforcementAction(scopedActions) | ||
if err != nil { | ||
return nil, fmt.Errorf("%w: %w", ErrInvalidConstraint, err) | ||
} | ||
|
||
// Flag to indicate if all enforcement points should be enforced | ||
enforceAll := false | ||
// Initialize a map to hold enforcement actions for each enforcement point | ||
actionsForEPs := make(map[string]map[string]bool) | ||
// Populate the actionsForEPs map with enforcement points from eps, initializing their action maps | ||
for _, enforcementPoint := range eps { | ||
if enforcementPoint == AllEnforcementPoints { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there some reason AllEnforcementPoints would be passed to this function? The behavior if "*" is passed here and also exists in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was considering that |
||
enforceAll = true // Set enforceAll to true if the special identifier for all enforcement points is found | ||
} | ||
actionsForEPs[enforcementPoint] = make(map[string]bool) // Initialize the action map for the enforcement point | ||
} | ||
|
||
// Iterate over the scoped enforcement actions to populate actions for each enforcement point | ||
for _, scopedEA := range scopedEnforcementActions { | ||
for _, enforcementPoint := range scopedEA.EnforcementPoints { | ||
epName := strings.ToLower(enforcementPoint.Name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we want to do this here since GetEnforcementAction today just passes the value thru as is.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since EP can be any string, my preference would be to not converting it to lowercase to preserve it. how do others feel? @maxsmythe @sozercan ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm down with preserving case |
||
ea := strings.ToLower(scopedEA.Action) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency with pre-existing code, case-sensitive comparison (see G8r code link above) |
||
// If enforceAll is true, or the enforcement point is explicitly listed, initialize its action map | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is inaccurate (does not initialize if EP is explicitly listed), and also unnecessary, since it merely describes the if statement and does not explain anything nuanced or unintuitive. |
||
if _, ok := actionsForEPs[epName]; !ok && enforceAll { | ||
actionsForEPs[epName] = make(map[string]bool) | ||
} | ||
// Skip adding actions for enforcement points not in the list unless enforceAll is true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unnecessary comment |
||
if _, ok := actionsForEPs[epName]; !ok && epName != AllEnforcementPoints { | ||
continue | ||
} | ||
// If the enforcement point is the special identifier for all, apply the action to all enforcement points | ||
switch epName { | ||
case AllEnforcementPoints: | ||
for ep := range actionsForEPs { | ||
actionsForEPs[ep][ea] = true | ||
} | ||
default: | ||
actionsForEPs[epName][ea] = true | ||
} | ||
} | ||
} | ||
|
||
return actionsForEPs, nil | ||
} | ||
|
||
// Helper function to access nested fields as an array. | ||
func getNestedFieldAsArray(obj map[string]interface{}, fields ...string) ([]interface{}, bool, error) { | ||
value, found, err := unstructured.NestedFieldNoCopy(obj, fields...) | ||
if err != nil { | ||
return nil, false, err | ||
} | ||
if !found { | ||
return nil, false, nil | ||
} | ||
if arr, ok := value.([]interface{}); ok { | ||
return arr, true, nil | ||
} | ||
return nil, false, nil | ||
} | ||
|
||
// Helper function to convert a value to a []ScopedEnforcementAction. | ||
func convertToSliceScopedEnforcementAction(value interface{}) ([]ScopedEnforcementAction, error) { | ||
var result []ScopedEnforcementAction | ||
if arr, ok := value.([]interface{}); ok { | ||
for _, v := range arr { | ||
if m, ok := v.(map[string]interface{}); ok { | ||
ritazh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
scopedEA := &ScopedEnforcementAction{} | ||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, scopedEA); err != nil { | ||
return nil, err | ||
} | ||
result = append(result, *scopedEA) | ||
} else { | ||
return nil, fmt.Errorf("scopedEnforcementActions value must be a []scopedEnforcementAction{action: string, enforcementPoints: []EnforcementPoint{name: string}}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can consider checking these conditions first, then return. this will reduce the number of elses you need in the code. |
||
} | ||
} | ||
return result, nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
} | ||
return nil, fmt.Errorf("scopedEnforcementActions value must be a []scopedEnforcementAction{action: string, enforcementPoints: []EnforcementPoint{name: string}}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package constraints | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
) | ||
|
||
const ( | ||
// WebhookEnforcementPoint is the enforcement point for admission. | ||
WebhookEnforcementPoint = "validation.gatekeeper.sh" | ||
|
||
// AuditEnforcementPoint is the enforcement point for audit. | ||
AuditEnforcementPoint = "audit.gatekeeper.sh" | ||
|
||
// GatorEnforcementPoint is the enforcement point for gator cli. | ||
GatorEnforcementPoint = "gator.gatekeeper.sh" | ||
) | ||
|
||
func TestGetEnforcementActionsForEP(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
constraint *unstructured.Unstructured | ||
eps []string | ||
expected map[string]map[string]bool | ||
err error | ||
}{ | ||
{ | ||
name: "wildcard enforcement point", | ||
constraint: &unstructured.Unstructured{ | ||
Object: map[string]interface{}{ | ||
"spec": map[string]interface{}{ | ||
"scopedEnforcementActions": []interface{}{ | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": AuditEnforcementPoint, | ||
}, | ||
map[string]interface{}{ | ||
"name": WebhookEnforcementPoint, | ||
}, | ||
}, | ||
"action": "warn", | ||
}, | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": "*", | ||
}, | ||
}, | ||
"action": "deny", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
expected: map[string]map[string]bool{ | ||
AuditEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
WebhookEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
GatorEnforcementPoint: { | ||
"deny": true, | ||
}, | ||
}, | ||
eps: []string{AuditEnforcementPoint, WebhookEnforcementPoint, GatorEnforcementPoint}, | ||
}, | ||
{ | ||
name: "Actions for selective enforcement point with case sensitive input", | ||
constraint: &unstructured.Unstructured{ | ||
Object: map[string]interface{}{ | ||
"spec": map[string]interface{}{ | ||
"scopedEnforcementActions": []interface{}{ | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": AuditEnforcementPoint, | ||
}, | ||
map[string]interface{}{ | ||
"name": "Validation.Gatekeeper.Sh", | ||
}, | ||
}, | ||
"action": "Warn", | ||
}, | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": "*", | ||
}, | ||
}, | ||
"action": "deny", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
expected: map[string]map[string]bool{ | ||
ritazh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
WebhookEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
GatorEnforcementPoint: { | ||
"deny": true, | ||
}, | ||
}, | ||
eps: []string{WebhookEnforcementPoint, GatorEnforcementPoint}, | ||
}, | ||
{ | ||
name: "wildcard enforcement point in scoped enforcement action, get actions for all enforcement points", | ||
constraint: &unstructured.Unstructured{ | ||
Object: map[string]interface{}{ | ||
"spec": map[string]interface{}{ | ||
"scopedEnforcementActions": []interface{}{ | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": AuditEnforcementPoint, | ||
}, | ||
map[string]interface{}{ | ||
"name": WebhookEnforcementPoint, | ||
}, | ||
}, | ||
"action": "warn", | ||
}, | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": AllEnforcementPoints, | ||
}, | ||
}, | ||
"action": "deny", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
expected: map[string]map[string]bool{ | ||
AuditEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
WebhookEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
AllEnforcementPoints: { | ||
"deny": true, | ||
}, | ||
}, | ||
eps: []string{AllEnforcementPoints}, | ||
}, | ||
{ | ||
name: "wildcard enforcement point in scoped enforcement action, get actions for two enforcement points", | ||
constraint: &unstructured.Unstructured{ | ||
Object: map[string]interface{}{ | ||
"spec": map[string]interface{}{ | ||
"scopedEnforcementActions": []interface{}{ | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": AuditEnforcementPoint, | ||
}, | ||
map[string]interface{}{ | ||
"name": WebhookEnforcementPoint, | ||
}, | ||
}, | ||
"action": "warn", | ||
}, | ||
map[string]interface{}{ | ||
"enforcementPoints": []interface{}{ | ||
map[string]interface{}{ | ||
"name": AllEnforcementPoints, | ||
}, | ||
}, | ||
"action": "deny", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
expected: map[string]map[string]bool{ | ||
AuditEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
WebhookEnforcementPoint: { | ||
"warn": true, | ||
"deny": true, | ||
}, | ||
}, | ||
eps: []string{WebhookEnforcementPoint, AuditEnforcementPoint}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
actions, err := GetEnforcementActionsForEP(tt.constraint, tt.eps) | ||
if err != nil { | ||
t.Errorf("Unexpected error: %v", err) | ||
} | ||
|
||
if !reflect.DeepEqual(actions, tt.expected) { | ||
t.Errorf("Expected %v, got %v", tt.expected, actions) | ||
} | ||
}) | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like other checks for enforcement action are case sensitive. We should follow the same pattern.
https://github.com/open-policy-agent/gatekeeper/blob/bd96c5263523be54642dcb4a88c2a9e502a74ea7/pkg/webhook/policy.go#L300-L309
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maxsmythe were you meaning to refer to some other codeblock? I tested the switch in the code you shared, it is defaulting when
r.enforcementAction
is mis-matching withstring(util.Dryrun)
orstring(util.Warn)
with case sensitiver.enforcementAction
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JaydipGabani From the code Max linked, it means we should preserve the case sensitivity of enforcementAction instead of converting it to all lowercase. Currently, only these are expected https://github.com/open-policy-agent/gatekeeper/blob/master/pkg/util/enforcement_action.go#L14-L18 so unless users pass in
deny
,dryrun
,warn
,scoped
(new one that we should add to that list), we should set it asunrecognized
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ritazh Thanks for the clarification. Sorry for being confused. Checking for case sensitve matching for actions now.