Skip to content

Commit

Permalink
feat: Notation image validation as opa function
Browse files Browse the repository at this point in the history
  • Loading branch information
mxab committed Jan 6, 2024
1 parent 73e0796 commit 926a4cf
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 18 deletions.
5 changes: 3 additions & 2 deletions admissionctrl/mutator/opa_json_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/api"
"github.com/mxab/nacp/admissionctrl/notation"
"github.com/mxab/nacp/admissionctrl/opa"
)

Expand Down Expand Up @@ -79,11 +80,11 @@ func (j *OpaJsonPatchMutator) Name() string {
return j.name
}

func NewOpaJsonPatchMutator(name, filename, query string, logger hclog.Logger) (*OpaJsonPatchMutator, error) {
func NewOpaJsonPatchMutator(name, filename, query string, logger hclog.Logger, ImageVerifier notation.ImageVerifier) (*OpaJsonPatchMutator, error) {

ctx := context.TODO()
// read the policy file
preparedQuery, err := opa.CreateQuery(filename, query, ctx)
preparedQuery, err := opa.CreateQuery(filename, query, ctx, ImageVerifier)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion admissionctrl/mutator/opa_json_patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestJSONPatcher_Mutate(t *testing.T) {

func newMutator(t *testing.T, filename, query string) *OpaJsonPatchMutator {
t.Helper()
m, err := NewOpaJsonPatchMutator("testopavalidator", filename, query, hclog.NewNullLogger())
m, err := NewOpaJsonPatchMutator("testopavalidator", filename, query, hclog.NewNullLogger(), nil)
require.NoError(t, err)
return m
}
20 changes: 19 additions & 1 deletion admissionctrl/opa/opa.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"os"

"github.com/hashicorp/nomad/api"
"github.com/mxab/nacp/admissionctrl/notation"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"
)

type OpaQuery struct {
Expand All @@ -16,7 +19,7 @@ type OpaQueryResult struct {
resultSet *rego.ResultSet
}

func CreateQuery(filename string, query string, ctx context.Context) (*OpaQuery, error) {
func CreateQuery(filename string, query string, ctx context.Context, verifier notation.ImageVerifier) (*OpaQuery, error) {

module, err := os.ReadFile(filename)
if err != nil {
Expand All @@ -25,6 +28,21 @@ func CreateQuery(filename string, query string, ctx context.Context) (*OpaQuery,

preparedQuery, err := rego.New(
rego.Query(query),
rego.Function1(
&rego.Function{
Name: "validNotationImage",
Decl: types.NewFunction(types.Args(types.S), types.B),
},
func(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
if str, ok := a.Value.(ast.String); ok {
ctx := bctx.Context
err := verifier.VerifyImage(ctx, string(str))
valid := err == nil
return ast.BooleanTerm(valid), nil

}
return ast.BooleanTerm(false), nil
}),
rego.Module(filename, string(module)),
).PrepareForEval(ctx)

Expand Down
77 changes: 74 additions & 3 deletions admissionctrl/opa/opa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package opa

import (
"context"
"errors"
"testing"

"github.com/hashicorp/nomad/api"
Expand All @@ -19,7 +20,7 @@ func TestOpa(t *testing.T) {
warnings = data.opatest.warnings
patch = data.opatest.patch
`, ctx)
`, ctx, nil)
require.Nil(t, err, "No error creating query")
assert.NotNil(t, query, "Query is not nil")

Expand Down Expand Up @@ -51,7 +52,7 @@ func TestFailOnEmptyResultSet(t *testing.T) {
errors = data.opatest.notexisting
`, ctx)
`, ctx, nil)
require.Nil(t, err, "No error creating query")
assert.NotNil(t, query, "Query is not nil")

Expand All @@ -69,7 +70,7 @@ func TestReturnsEmptyIfNotExisting(t *testing.T) {
notimportant = data.opatest.errors
`, ctx)
`, ctx, nil)
require.Nil(t, err, "No error creating query")
assert.NotNil(t, query, "Query is not nil")
job := &api.Job{}
Expand All @@ -85,3 +86,73 @@ func TestReturnsEmptyIfNotExisting(t *testing.T) {
assert.Equal(t, []interface{}{}, patch, "Patch is correct")

}

type DummyVerifier struct {
}

func (m *DummyVerifier) VerifyImage(ctx context.Context, imageReference string) error {

if imageReference == "invalidimage:latest" {
return errors.New("invalid image")
}
if imageReference == "validimage:latest" {
return nil
}

panic("invalid image reference")
}
func TestNotationImageValidation(t *testing.T) {

tt := []struct {
name string
image string
expectedErrors []interface{}
}{
{
name: "valid image",
image: "validimage:latest",
expectedErrors: []interface{}{},
},
{
name: "invalid image",
image: "invalidimage:latest",
expectedErrors: []interface{}{
"Image is not in valid",
},
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {

ctx := context.Background()

path := testutil.Filepath(t, "opa/test_notation.rego")

query, err := CreateQuery(path, `
errors = data.opatest.errors
`, ctx, new(DummyVerifier))
job := &api.Job{
TaskGroups: []*api.TaskGroup{
{
Tasks: []*api.Task{
{
Driver: "docker",
Config: map[string]interface{}{
"image": tc.image,
},
},
},
},
},
}
require.NoError(t, err, "No error creating query")
result, err := query.Query(ctx, job)
require.NoError(t, err, "No error executing query")
require.NotNil(t, result, "Result is not nil")

errors := result.GetErrors()
assert.Equal(t, tc.expectedErrors, errors, "Errors are correct")
})
}
}
5 changes: 3 additions & 2 deletions admissionctrl/validator/opa_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/api"
"github.com/mxab/nacp/admissionctrl/notation"
"github.com/mxab/nacp/admissionctrl/opa"
)

Expand Down Expand Up @@ -65,12 +66,12 @@ func (v *OpaValidator) Name() string {
return v.name
}

func NewOpaValidator(name, filename, query string, logger hclog.Logger) (*OpaValidator, error) {
func NewOpaValidator(name, filename, query string, logger hclog.Logger, imageVerifier notation.ImageVerifier) (*OpaValidator, error) {

ctx := context.TODO()

// read the policy file
preparedEvalQuery, err := opa.CreateQuery(filename, query, ctx)
preparedEvalQuery, err := opa.CreateQuery(filename, query, ctx, imageVerifier)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions admissionctrl/validator/opa_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestOpaValidator(t *testing.T) {

// create a new OPA object
opa, err := NewOpaValidator("testopavalidator", testutil.Filepath(t, "opa/validators/prefixed_policies.rego"),
"errors = data.prefixed_policies.errors", hclog.NewNullLogger())
"errors = data.prefixed_policies.errors", hclog.NewNullLogger(), nil)

require.Equal(t, nil, err)

Expand Down Expand Up @@ -91,7 +91,7 @@ func TestOpaValidatorSimple(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {

opa, err := NewOpaValidator("testopavalidator", testutil.Filepath(t, "opa/errors.rego"),
tt.query, hclog.NewNullLogger())
tt.query, hclog.NewNullLogger(), nil)
require.NoError(t, err)
warnings, err := opa.Validate(dummyJob)
require.Equal(t, tt.wantErr, err != nil, "OpaValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
Expand Down
24 changes: 19 additions & 5 deletions cmd/nacp/nacp.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,11 @@ func createMutators(c *config.Config, logger hclog.Logger) ([]admissionctrl.JobM
switch m.Type {

case "opa_json_patch":

mutator, err := mutator.NewOpaJsonPatchMutator(m.Name, m.OpaRule.Filename, m.OpaRule.Query, logger.Named("opa_mutator"))
notationVerifier, err := buildVerifierIfEnabled(m.OpaRule.Notation, logger.Named("notation_verifier"))
if err != nil {
return nil, err
}
mutator, err := mutator.NewOpaJsonPatchMutator(m.Name, m.OpaRule.Filename, m.OpaRule.Query, logger.Named("opa_mutator"), notationVerifier)
if err != nil {
return nil, err
}
Expand All @@ -567,8 +570,11 @@ func createValidators(c *config.Config, logger hclog.Logger) ([]admissionctrl.Jo
for _, v := range c.Validators {
switch v.Type {
case "opa":

opaValidator, err := validator.NewOpaValidator(v.Name, v.OpaRule.Filename, v.OpaRule.Query, logger.Named("opa_validator"))
notationVerifier, err := buildVerifierIfEnabled(v.Notation, logger.Named("notation_verifier"))
if err != nil {
return nil, err
}
opaValidator, err := validator.NewOpaValidator(v.Name, v.OpaRule.Filename, v.OpaRule.Query, logger.Named("opa_validator"), notationVerifier)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -596,9 +602,17 @@ func createValidators(c *config.Config, logger hclog.Logger) ([]admissionctrl.Jo
}
return jobValidators, nil
}

func buildVerifierIfEnabled(notationVerifierConfig *config.NotationVerifierConfig, logger hclog.Logger) (notation.ImageVerifier, error) {
if notationVerifierConfig == nil {
return nil, nil
}
return buildVerifier(notationVerifierConfig, logger)
}
func buildVerifier(notationVerifierConfig *config.NotationVerifierConfig, logger hclog.Logger) (notation.ImageVerifier, error) {

if notationVerifierConfig == nil {
return nil, fmt.Errorf("notation verifier config is nil")
}
policy, err := notation.LoadTrustPolicyDocument(notationVerifierConfig.TrustPolicyFile)
if err != nil {
return nil, err
Expand Down
5 changes: 3 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ type Webhook struct {
Method string `hcl:"method"`
}
type OpaRule struct {
Query string `hcl:"query"`
Filename string `hcl:"filename"`
Query string `hcl:"query"`
Filename string `hcl:"filename"`
Notation *NotationVerifierConfig `hcl:"notation,block"`
}

type Validator struct {
Expand Down
9 changes: 9 additions & 0 deletions testdata/opa/test_notation.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package opatest

errors[errMsg] {

image := input.TaskGroups[0].Tasks[0].Config.image

not validNotationImage(image)
errMsg := "Image is not in valid"
}

0 comments on commit 926a4cf

Please sign in to comment.