Skip to content

Commit

Permalink
feat: adding scopedenforcementactions (open-policy-agent#403)
Browse files Browse the repository at this point in the history
* adding scopedenforcementactions

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* adding tests

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* fixing tests

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* refactoring template client and query

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* refatoring queryopts to reviewopts

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* updating ccomments

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* removing EP variables

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* generic webhook EP

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* simplifying constraintToBinding

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* adding comments

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* checking lowercase eps, fixing nil variable access

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* fixing file perm

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* adding tests for case sensitivity and missing enforcementaction

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* updating gk-webhook EP

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* fixing test

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* adding enforcement action and scoped enforcement actions in result and response spec

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* fixing faulty test

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* refactoring code for simplycity

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* removing comments, removing * from review opts and client opts

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* mandating client to be initialized with enforcment point

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* case sensitive check for actions

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* updating code comments

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* removing comments

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* removing enforcement action check while adding constriant to template client

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* removing all enforcement points from matches

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

* preserving enforcement point names

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>

---------

Signed-off-by: Jaydip Gabani <gabanijaydip@gmail.com>
Signed-off-by: Jaydipkumar Arvindbhai Gabani <gabanijaydip@gmail.com>
  • Loading branch information
JaydipGabani committed Aug 2, 2024
1 parent a05810c commit 97d977e
Show file tree
Hide file tree
Showing 31 changed files with 979 additions and 199 deletions.
1 change: 1 addition & 0 deletions constraint/deploy/tools.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build tools
// +build tools

// This existence of this package allows vendoring of the manifests in this directory by go 1.13+.
Expand Down
117 changes: 114 additions & 3 deletions constraint/pkg/apis/constraints/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,37 @@ import (
"fmt"

"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 EnforcementAction string

type EnforcementPoint struct {
Name string `json:"name"`
}

const (
// Group is the API Group of Constraints.
Group = "constraints.gatekeeper.sh"

// EnforcementActionDeny indicates that if a review fails validation for a
// AllEnforcementPoints is a wildcard to indicate all enforcement points.
AllEnforcementPoints = "*"
)

const (
// Deny indicates that if a review fails validation for a
// Constraint, that it should be rejected. Errors encountered running
// validation are treated as failing validation.
//
// This is the default EnforcementAction.
EnforcementActionDeny = "deny"
Deny EnforcementAction = "deny"
Warn EnforcementAction = "warn"
Scoped EnforcementAction = "scoped"
)

var (
Expand All @@ -26,6 +45,11 @@ 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")

ErrInvalidSpecEnforcementAction = errors.New("scopedEnforcementActions value must be a [{action: string, enforcementPoints: [{name: string}]}]")
)

// GetEnforcementAction returns a Constraint's enforcementAction, which indicates
Expand All @@ -40,8 +64,95 @@ func GetEnforcementAction(constraint *unstructured.Unstructured) (string, error)
}

if !found {
return EnforcementActionDeny, nil
return string(Deny), nil
}

return action, nil
}

func IsEnforcementActionScoped(action string) bool {
return action == string(Scoped)
}

// GetEnforcementActionsForEP returns a map of enforcement actions for enforcement points passed in.
func GetEnforcementActionsForEP(constraint *unstructured.Unstructured, eps []string) (map[string][]string, error) {
scopedActions, found, err := getNestedFieldAsArray(constraint.Object, "spec", "scopedEnforcementActions")
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidSpecEnforcementAction, err)
}
if !found {
return nil, fmt.Errorf("%w: spec.scopedEnforcementActions must be defined", ErrMissingRequiredField)
}

scopedEnforcementActions, err := convertToSliceScopedEnforcementAction(scopedActions)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidConstraint, err)
}

enforcementPointsToActionsMap := make(map[string]map[string]bool)
for _, ep := range eps {
enforcementPointsToActionsMap[ep] = make(map[string]bool)
}
for _, scopedEA := range scopedEnforcementActions {
for _, enforcementPoint := range scopedEA.EnforcementPoints {
epName := enforcementPoint.Name
if epName == AllEnforcementPoints {
for _, ep := range eps {
enforcementPointsToActionsMap[ep][scopedEA.Action] = true
}
break
}
if _, ok := enforcementPointsToActionsMap[epName]; ok {
enforcementPointsToActionsMap[epName][scopedEA.Action] = true
}
}
}
enforcementActionsForEPs := make(map[string][]string)
for ep, actions := range enforcementPointsToActionsMap {
if len(actions) == 0 {
continue
}
enforcementActionsForEPs[ep] = make([]string, 0, len(actions))
for action := range actions {
enforcementActionsForEPs[ep] = append(enforcementActionsForEPs[ep], action)
}
}

return enforcementActionsForEPs, 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
arr, ok := value.([]interface{})
if !ok {
return nil, ErrInvalidSpecEnforcementAction
}
for _, v := range arr {
m, ok := v.(map[string]interface{})
if !ok {
return nil, ErrInvalidSpecEnforcementAction
}
scopedEA := &ScopedEnforcementAction{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, scopedEA); err != nil {
return nil, err
}
result = append(result, *scopedEA)
}
return result, nil
}
213 changes: 213 additions & 0 deletions constraint/pkg/apis/constraints/apis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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{
WebhookEnforcementPoint: {
"deny": true,
},
GatorEnforcementPoint: {
"deny": true,
},
},
eps: []string{WebhookEnforcementPoint, GatorEnforcementPoint},
},
{
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)
}

got := make(map[string]map[string]bool)
for ep, actions := range actions {
got[ep] = make(map[string]bool)
for _, action := range actions {
got[ep][action] = true
}
}

if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, actions)
}
})
}
}

func TestIsEnforcementActionScoped(t *testing.T) {
tests := []struct {
name string
action string
want bool
}{
{
name: "scoped enforcement action",
action: "scoped",
want: true,
},
{
name: "Scoped enforcement action",
action: "Scoped",
want: false,
},
{
name: "Non-scoped enforcement action",
action: "Deny",
want: false,
},
{
name: "Empty enforcement action",
action: "",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsEnforcementActionScoped(tt.action)
if got != tt.want {
t.Errorf("Expected %v, got %v", tt.want, got)
}
})
}
}
1 change: 1 addition & 0 deletions constraint/pkg/apis/templates/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 97d977e

Please sign in to comment.