Skip to content

Commit

Permalink
Add ValidatingAdmissionPolicy generation function
Browse files Browse the repository at this point in the history
Signed-off-by: Max Smythe <[email protected]>
  • Loading branch information
maxsmythe committed Nov 14, 2023
1 parent af4fcdb commit 5e8e843
Show file tree
Hide file tree
Showing 16 changed files with 1,827 additions and 422 deletions.
4 changes: 2 additions & 2 deletions constraint/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.18
require (
github.com/davecgh/go-spew v1.1.1
github.com/golang/glog v1.1.1
github.com/google/cel-go v0.16.1
github.com/google/go-cmp v0.5.9
github.com/onsi/gomega v1.27.7
github.com/open-policy-agent/opa v0.57.1
Expand All @@ -17,7 +16,7 @@ require (
k8s.io/apimachinery v0.28.3
k8s.io/apiserver v0.28.3
k8s.io/client-go v0.28.3
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
sigs.k8s.io/controller-runtime v0.15.0
sigs.k8s.io/yaml v1.3.0
)
Expand All @@ -44,6 +43,7 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/cel-go v0.16.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions constraint/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 h1:trsWhjU5jZrx6UvFu4WzQDrN7Pga4a7Qg+zcfcj64PA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2/go.mod h1:+qG7ISXqCDVVcyO8hLn12AKVYYUjM7ftlqsqmrhMZE0=
sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU=
Expand Down
185 changes: 11 additions & 174 deletions constraint/pkg/client/drivers/k8scel/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package k8scel

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
Expand All @@ -12,21 +11,17 @@ import (
apiconstraints "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints"
"github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers"
pSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/k8scel/schema"
"github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/k8scel/transform"
"github.com/open-policy-agent/frameworks/constraint/pkg/core/templates"
"github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation"
"github.com/open-policy-agent/frameworks/constraint/pkg/types"
"github.com/open-policy-agent/opa/storage"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
celAPI "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/cel/environment"
)

Expand Down Expand Up @@ -65,41 +60,24 @@ func (d *Driver) Name() string {
}

func (d *Driver) AddTemplate(_ context.Context, ct *templates.ConstraintTemplate) error {
if len(ct.Spec.Targets) != 1 {
return errors.New("wrong number of targets defined, only 1 target allowed")
}

var source *pSchema.Source
for _, code := range ct.Spec.Targets[0].Code {
if code.Engine != pSchema.Name {
continue
}
var err error
source, err = pSchema.GetSource(code)
if err != nil {
return err
}
break
}
if source == nil {
return errors.New("K8sNativeValidation code not defined")
source, err := pSchema.GetSourceFromTemplate(ct)
if err != nil {
return err
}

// FRICTION: Note that compilation errors are possible, but we cannot introspect to see whether any
// occurred
celVars := cel.OptionalVariableDeclarations{}

// We don't want to have access to parameters for anything other than driver-defined variables, so we
// We don't want to have access to parameters for anything other than driver-defined logic, so we
// can keep the user from accessing the full constraint schema.
celVarsWithParameters := cel.OptionalVariableDeclarations{HasParams: true}

// TODO add support for user-defined variables
vapVars := []cel.NamedExpressionAccessor{
&validatingadmissionpolicy.Variable{
Name: "params",
Expression: "params.spec.parameters",
},
vapVars, err := source.GetVariables()
if err != nil {
return err
}
vapVars = append(vapVars, transform.AllVariablesCEL()...)
filterCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
if err != nil {
return err
Expand Down Expand Up @@ -180,7 +158,7 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru
return nil, errors.New("cannot convert review to ARGetter")
}
aRequest := arGetter.GetAdmissionRequest()
request, err := NewWrapper(aRequest)
versionedAttr, err := transform.RequestToVersionedAttributes(aRequest)
if err != nil {
return nil, err
}
Expand All @@ -189,26 +167,14 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru

for _, constraint := range constraints {
evalStartTime := time.Now()
// FRICTION/design question: should parameters be created as a "mock" object so that users don't have to type `params.spec.parameters`? How do we prevent visibility into other,
// non-parameter fields, such as `spec.match`? Does it matter? Note that creating a special "parameters" object means that we'd need to copy the constraint contents to
// a special "parameters" object for on-server enforcement with a clean value for "params", which is non-ideal. Could we provide the field of the parameters object and limit scoping to that?
// Then how would we implement custom matchers? Maybe adding variable assignments to the Policy Definition is a better idea? That would at least allow for a convenience handle, even if
// it doesn't scope visibility.

// template name is the lowercase of its kind
validator := d.validators[strings.ToLower(constraint.GetKind())]
if validator == nil {
return nil, fmt.Errorf("unknown constraint template validator: %s", constraint.GetKind())
}
versionedAttr := &admission.VersionedAttributes{
Attributes: request,
VersionedKind: request.GetKind(),
VersionedOldObject: request.GetOldObject(),
VersionedObject: request.GetObject(),
}

// TODO: should namespace be made available, if possible? Generally that context should be present
response := validator.Validate(ctx, request.GetResource(), versionedAttr, constraint, nil, celAPI.PerCallLimit, nil)
response := validator.Validate(ctx, versionedAttr.GetResource(), versionedAttr, constraint, nil, celAPI.PerCallLimit, nil)

enforcementAction, found, err := unstructured.NestedString(constraint.Object, "spec", "enforcementAction")
if err != nil {
Expand Down Expand Up @@ -265,132 +231,3 @@ func (d *Driver) GetDescriptionForStat(statName string) (string, error) {
type ARGetter interface {
GetAdmissionRequest() *admissionv1.AdmissionRequest
}

// FRICTION this wrapper class is excessive. Validator code should define an interface that only requires the methods it needs.
type RequestWrapper struct {
ar *admissionv1.AdmissionRequest
object runtime.Object
oldObject runtime.Object
operationOptions runtime.Object
}

func NewWrapper(req *admissionv1.AdmissionRequest) (*RequestWrapper, error) {
var object runtime.Object
if len(req.Object.Raw) != 0 {
object = &unstructured.Unstructured{}
if err := json.Unmarshal(req.Object.Raw, object); err != nil {
return nil, fmt.Errorf("%w: could not unmarshal object", err)
}
}

var oldObject runtime.Object
if len(req.OldObject.Raw) != 0 {
oldObject = &unstructured.Unstructured{}
if err := json.Unmarshal(req.OldObject.Raw, oldObject); err != nil {
return nil, fmt.Errorf("%w: could not unmarshal old object", err)
}
}

// this may be unnecessary, since GetOptions() may not be used by downstream
// code, but is better than doing this lazily and needing to panic if GetOptions()
// fails.
var options runtime.Object
if len(req.Options.Raw) != 0 {
options = &unstructured.Unstructured{}
if err := json.Unmarshal(req.Options.Raw, options); err != nil {
return nil, fmt.Errorf("%w: could not unmarshal options", err)
}
}
return &RequestWrapper{
ar: req,
object: object,
oldObject: oldObject,
operationOptions: options,
}, nil
}

func (w *RequestWrapper) GetName() string {
return w.ar.Name
}

func (w *RequestWrapper) GetNamespace() string {
return w.ar.Namespace
}

func (w *RequestWrapper) GetResource() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: w.ar.Resource.Group,
Version: w.ar.Resource.Version,
Resource: w.ar.Resource.Resource,
}
}

func (w *RequestWrapper) GetSubresource() string {
return w.ar.SubResource
}

var opMap = map[admissionv1.Operation]admission.Operation{
admissionv1.Create: admission.Create,
admissionv1.Update: admission.Update,
admissionv1.Delete: admission.Delete,
admissionv1.Connect: admission.Connect,
}

func (w *RequestWrapper) GetOperation() admission.Operation {
return opMap[w.ar.Operation]
}

func (w *RequestWrapper) GetOperationOptions() runtime.Object {
return w.operationOptions
}

func (w *RequestWrapper) IsDryRun() bool {
if w.ar.DryRun == nil {
return false
}
return *w.ar.DryRun
}

func (w *RequestWrapper) GetObject() runtime.Object {
return w.object
}

func (w *RequestWrapper) GetOldObject() runtime.Object {
return w.oldObject
}

func (w *RequestWrapper) GetKind() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: w.ar.Kind.Group,
Version: w.ar.Kind.Version,
Kind: w.ar.Kind.Kind,
}
}

func (w *RequestWrapper) GetUserInfo() user.Info {
extra := map[string][]string{}
for k := range w.ar.UserInfo.Extra {
vals := make([]string, len(w.ar.UserInfo.Extra[k]))
copy(vals, w.ar.UserInfo.Extra[k])
extra[k] = vals
}

return &user.DefaultInfo{
Name: w.ar.UserInfo.Username,
UID: w.ar.UserInfo.UID,
Groups: w.ar.UserInfo.Groups,
Extra: extra,
}
}

func (w *RequestWrapper) AddAnnotation(_, _ string) error {
return errors.New("AddAnnotation not implemented")
}

func (w *RequestWrapper) AddAnnotationWithLevel(_, _ string, _ auditinternal.Level) error {
return errors.New("AddAnnotationWithLevel not implemented")
}

func (w *RequestWrapper) GetReinvocationContext() admission.ReinvocationContext {
return nil
}
9 changes: 9 additions & 0 deletions constraint/pkg/client/drivers/k8scel/schema/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package schema

import "errors"

var (
ErrBadMatchCondition = errors.New("invalid match condition")
ErrBadVariable = errors.New("invalid variable definition")
ErrBadFailurePolicy = errors.New("invalid failure policy")
)
Loading

0 comments on commit 5e8e843

Please sign in to comment.