Skip to content

Commit

Permalink
feat: Validate policy and test files with JSON schema (#1526)
Browse files Browse the repository at this point in the history
* enhancement: Validate policy and test files with JSON schema

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>

* Address reviews

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>

* Address reviews

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>

* Address reviews

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>

* Add empty path handling for validation error

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>

* Address reviews

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>

---------

Signed-off-by: Oğuzhan Durgun <oguzhandurgun95@gmail.com>
  • Loading branch information
oguzhand95 committed Apr 17, 2023
1 parent 16d8656 commit 7570df3
Show file tree
Hide file tree
Showing 21 changed files with 184 additions and 29 deletions.
9 changes: 5 additions & 4 deletions cmd/cerbos/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
"github.com/cerbos/cerbos/internal/engine"
"github.com/cerbos/cerbos/internal/outputcolor"
"github.com/cerbos/cerbos/internal/printer"
"github.com/cerbos/cerbos/internal/schema"
internalschema "github.com/cerbos/cerbos/internal/schema"
"github.com/cerbos/cerbos/internal/storage/disk"
"github.com/cerbos/cerbos/internal/storage/index"
"github.com/cerbos/cerbos/internal/util"
Expand Down Expand Up @@ -97,11 +97,11 @@ func (c *Cmd) Run(k *kong.Kong) error {

store := disk.NewFromIndexWithConf(idx, &disk.Conf{})

enforcement := schema.EnforcementReject
enforcement := internalschema.EnforcementReject
if c.IgnoreSchemas {
enforcement = schema.EnforcementNone
enforcement = internalschema.EnforcementNone
}
schemaMgr := schema.NewFromConf(ctx, store, schema.NewConf(enforcement))
schemaMgr := internalschema.NewFromConf(ctx, store, internalschema.NewConf(enforcement))

if err := compile.BatchCompile(idx.GetAllCompilationUnits(ctx), schemaMgr); err != nil {
compErr := new(compile.ErrorList)
Expand Down Expand Up @@ -141,6 +141,7 @@ func (c *Cmd) Run(k *kong.Kong) error {
if err != nil {
return err
}

results, err := verify.Verify(ctx, testFsys, eng, verifyConf)
if err != nil {
return fmt.Errorf("failed to run tests: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ require (
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
google.golang.org/grpc v1.54.0
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.11.2
modernc.org/sqlite v1.21.1
)
Expand Down Expand Up @@ -268,7 +268,7 @@ require (
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
Expand Down
3 changes: 2 additions & 1 deletion internal/audit/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ package audit
import (
"fmt"

"gopkg.in/yaml.v3"

"github.com/cerbos/cerbos/internal/config"
"gopkg.in/yaml.v2"
)

const (
Expand Down
129 changes: 129 additions & 0 deletions internal/jsonschema/jsonschema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2021-2023 Zenauth Ltd.
// SPDX-License-Identifier: Apache-2.0

package jsonschema

import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"strings"

"github.com/santhosh-tekuri/jsonschema/v5"
"gopkg.in/yaml.v3"

"github.com/cerbos/cerbos/schema"
)

var (
policySchema *jsonschema.Schema
testSchema *jsonschema.Schema
)

func init() {
var err error
if policySchema, err = jsonschema.CompileString("Policy.schema.json", schema.PolicyJSONSchema); err != nil {
log.Fatalf("failed to compile policy schema: %v", err)
}

if testSchema, err = jsonschema.CompileString("TestSuite.schema.json", schema.TestSuiteJSONSchema); err != nil {
log.Fatalf("failed to compile test schema: %v", err)
}
}

// ValidatePolicy validates the policy in the fsys with the JSON schema.
func ValidatePolicy(fsys fs.FS, path string) error {
return validate(policySchema, fsys, path)
}

// ValidateTest validates the test in the fsys with the JSON schema.
func ValidateTest(fsys fs.FS, path string) error {
return validate(testSchema, fsys, path)
}

func validate(s *jsonschema.Schema, fsys fs.FS, path string) error {
f, err := fsys.Open(path)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", path, err)
}
defer f.Close()

data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}

var y any
if err := yaml.Unmarshal(data, &y); err != nil {
return fmt.Errorf("failed to unmarshal file %s: %w", path, err)
}

if err := s.Validate(y); err != nil {
var validationErr *jsonschema.ValidationError
if ok := errors.As(err, &validationErr); !ok {
return fmt.Errorf("unable to validate file %s: %w", path, err)
}

return newValidationErrorList(validationErr)
}

return nil
}

func newValidationError(err *jsonschema.ValidationError) validationError {
path := "/"
if err.InstanceLocation != "" {
path = err.InstanceLocation
}
return validationError{
Path: path,
Message: err.Message,
}
}

func newValidationErrorList(validationErr *jsonschema.ValidationError) validationErrorList {
if validationErr == nil {
return nil
}

if len(validationErr.Causes) == 0 {
return validationErrorList{newValidationError(validationErr)}
}

var errs validationErrorList
for _, err := range validationErr.Causes {
errs = append(errs, newValidationErrorList(err)...)
}

return errs
}

type validationError struct {
Path string
Message string
}

func (e validationError) Error() string {
return fmt.Sprintf("%s: %s", e.Path, e.Message)
}

type validationErrorList []validationError

func (e validationErrorList) Error() string {
return fmt.Sprintf("file is not valid: [%s]", strings.Join(e.ErrorMessages(), ", "))
}

func (e validationErrorList) ErrorMessages() []string {
if len(e) == 0 {
return nil
}

msgs := make([]string, len(e))
for i, err := range e {
msgs[i] = err.Error()
}

return msgs
}
2 changes: 1 addition & 1 deletion internal/storage/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package storage
import (
"fmt"

"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"

"github.com/cerbos/cerbos/internal/config"
)
Expand Down
6 changes: 6 additions & 0 deletions internal/storage/index/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

policyv1 "github.com/cerbos/cerbos/api/genpb/cerbos/policy/v1"
runtimev1 "github.com/cerbos/cerbos/api/genpb/cerbos/runtime/v1"
internaljsonschema "github.com/cerbos/cerbos/internal/jsonschema"
"github.com/cerbos/cerbos/internal/namer"
"github.com/cerbos/cerbos/internal/observability/metrics"
"github.com/cerbos/cerbos/internal/policy"
Expand Down Expand Up @@ -104,6 +105,11 @@ func build(ctx context.Context, fsys fs.FS, opts buildOptions) (Index, error) {
return nil
}

if err := internaljsonschema.ValidatePolicy(fsys, filePath); err != nil {
ib.addLoadFailure(filePath, err)
return nil
}

p := &policyv1.Policy{}
if err := util.LoadFromJSONOrYAML(fsys, filePath, p); err != nil {
ib.addLoadFailure(filePath, err)
Expand Down
4 changes: 2 additions & 2 deletions internal/test/testdata/index/corrupt_files.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
wantErrList:
loadFailures:
- error: |-
failed to unmarshal JSON: proto: (line 1:2): unknown field "key"
file is not valid: [/: missing properties: 'apiVersion', /: additionalProperties 'key' not allowed, /: missing properties: 'resourcePolicy', /: missing properties: 'principalPolicy', /: missing properties: 'derivedRoles']
file: principal.json
- error: |-
failed to unmarshal JSON: proto: (line 1:2): unknown field "some"
file is not valid: [/: missing properties: 'apiVersion', /: additionalProperties 'some' not allowed, /: missing properties: 'resourcePolicy', /: missing properties: 'principalPolicy', /: missing properties: 'derivedRoles']
file: resource.yaml
files:
"resource.yaml": |-
Expand Down
4 changes: 2 additions & 2 deletions internal/test/testdata/index/incomplete_files.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
wantErrList:
loadFailures:
- error: "invalid Policy.DerivedRoles: embedded message failed validation | caused by: invalid DerivedRoles.Definitions: value must contain at least 1 item(s)"
- error: "file is not valid: [/derivedRoles: missing properties: 'definitions']"
file: derived.yaml
- error: "invalid Policy.PolicyType: value is required"
- error: "file is not valid: [/: missing properties: 'resourcePolicy', /: missing properties: 'principalPolicy', /: missing properties: 'derivedRoles']"
file: resource.yaml
files:
"resource.yaml": |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ playgroundEvaluate:
"errors": [
{
"file": "resource.yaml",
"error": "Failed to read: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet"
"error": "Failed to read: failed to unmarshal file resource.yaml: yaml: invalid leading UTF-8 octet"
},
{
"file": "common_roles.yaml",
"error": "Failed to read: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet"
"error": "Failed to read: failed to unmarshal file common_roles.yaml: yaml: invalid leading UTF-8 octet"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ playgroundTest:
"errors": [
{
"file": "resource.yaml",
"error": "Failed to read: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet"
"error": "Failed to read: failed to unmarshal file resource.yaml: yaml: invalid leading UTF-8 octet"
},
{
"file": "common_roles.yaml",
"error": "Failed to read: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet"
"error": "Failed to read: failed to unmarshal file common_roles.yaml: yaml: invalid leading UTF-8 octet"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ playgroundTest:
{
"file": "policy_04_test.yaml",
"name": "Unknown",
"error": "failed to load test suite: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet",
"error": "failed to unmarshal file policy_04_test.yaml: yaml: invalid leading UTF-8 octet",
"summary": {
"overallResult": "RESULT_ERRORED"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ playgroundValidate:
"errors": [
{
"file": "resource.yaml",
"error": "Failed to read: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet"
"error": "Failed to read: failed to unmarshal file resource.yaml: yaml: invalid leading UTF-8 octet"
},
{
"file": "common_roles.yaml",
"error": "Failed to read: failed to convert YAML to JSON: yaml: invalid leading UTF-8 octet"
"error": "Failed to read: failed to unmarshal file common_roles.yaml: yaml: invalid leading UTF-8 octet"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: "api.cerbos.dev/v1"
description: |-
Common dynamic roles used within the Apatr app
derived_roles:
derivedRoles:
name: apatr_common_roles
definitions:
- name: owner
Expand Down
4 changes: 2 additions & 2 deletions internal/test/testdata/store_archive/policies.tar
Git LFS file not shown
4 changes: 2 additions & 2 deletions internal/test/testdata/store_archive/policies.tgz
Git LFS file not shown
4 changes: 2 additions & 2 deletions internal/test/testdata/store_archive/policies.zip
Git LFS file not shown
2 changes: 1 addition & 1 deletion internal/test/testdata/verify/cases/case_004.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"summary": {
"overallResult": "RESULT_ERRORED"
},
"error": "failed to load test suite: failed to unmarshal JSON: proto: syntax error (line 1:1): unexpected token \"x\""
"error": "file is not valid: [/: expected object, but got string]"
}
],
"summary": {
Expand Down
2 changes: 1 addition & 1 deletion internal/test/testdata/verify/cases/case_005.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"summary": {
"overallResult": "RESULT_ERRORED"
},
"error": "failed to load test suite: invalid TestSuite.Tests: value must contain at least 1 item(s)"
"error": "file is not valid: [/: missing properties: 'tests']"
}
],
"summary": {
Expand Down
2 changes: 1 addition & 1 deletion internal/test/testdata/verify/cases/case_007.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"summary": {
"overallResult": "RESULT_ERRORED"
},
"error": "failed to load test suite: failed to unmarshal JSON: proto: (line 1:84): invalid google.protobuf.Timestamp value \"blah\""
"error": "file is not valid: [/options/now: 'blah' is not valid 'date-time']"
}
],
"summary": {
Expand Down
12 changes: 12 additions & 0 deletions internal/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
policyv1 "github.com/cerbos/cerbos/api/genpb/cerbos/policy/v1"
"github.com/cerbos/cerbos/internal/engine"
internaljsonschema "github.com/cerbos/cerbos/internal/jsonschema"
"github.com/cerbos/cerbos/internal/util"
)

Expand Down Expand Up @@ -97,6 +98,17 @@ func Verify(ctx context.Context, fsys fs.FS, eng Checker, conf Config) (*policyv
}

runTestSuite := func(file string) *policyv1.TestResults_Suite {
if err := internaljsonschema.ValidateTest(fsys, file); err != nil {
return &policyv1.TestResults_Suite{
File: file,
Name: "Unknown",
Summary: &policyv1.TestResults_Summary{
OverallResult: policyv1.TestResults_RESULT_ERRORED,
},
Error: err.Error(),
}
}

suite := &policyv1.TestSuite{}
err := util.LoadFromJSONOrYAML(fsys, file, suite)
if err == nil {
Expand Down
6 changes: 6 additions & 0 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ var svcSwaggerRaw []byte
//go:embed assets/ui.html
var rapidocHTML []byte

//go:embed jsonschema/cerbos/policy/v1/Policy.schema.json
var PolicyJSONSchema string

//go:embed jsonschema/cerbos/policy/v1/TestSuite.schema.json
var TestSuiteJSONSchema string

func ServeSvcSwagger(w http.ResponseWriter, r *http.Request) {
defer cleanup(r)

Expand Down

0 comments on commit 7570df3

Please sign in to comment.