Skip to content

Commit

Permalink
Merge pull request #370 from sangkenlee/policy-validation
Browse files Browse the repository at this point in the history
Policy validation
  • Loading branch information
ktkfree authored Apr 11, 2024
2 parents 84df1ec + 0c986e5 commit 5b8a434
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 32 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ require (
github.com/swaggo/swag v1.16.3
github.com/thoas/go-funk v0.9.3
github.com/vmware-tanzu/cluster-api-provider-bringyourownhost v0.5.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/net v0.22.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
2 changes: 1 addition & 1 deletion internal/delivery/http/policy-template.go
Original file line number Diff line number Diff line change
Expand Up @@ -1735,7 +1735,7 @@ func (h *PolicyTemplateHandler) ExtractParameters(w http.ResponseWriter, r *http
return
}

if err := serializer.Map(r.Context(), response, &out); err != nil {
if err := serializer.Map(r.Context(), *response, &out); err != nil {
log.Info(r.Context(), err)
}

Expand Down
6 changes: 5 additions & 1 deletion internal/policy-template/paramdef-util.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,11 @@ func ParamDefsToJSONSchemaProeprties(paramdefs []*domain.ParameterDef) *apiexten
return nil
}

result := apiextensionsv1.JSONSchemaProps{Type: "object", Properties: convert(paramdefs)}
result := apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: convert(paramdefs),
AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{Allows: false},
}

return &result
}
Expand Down
94 changes: 93 additions & 1 deletion internal/policy-template/policy-template-rego.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package policytemplate

import (
"fmt"
"regexp"
"sort"
"strings"
Expand All @@ -14,7 +15,9 @@ const (
input_param_prefix = "input.parameters"
input_extract_pattern = `input(\.parameters|\[\"parameters\"\])((\[\"[\w\-]+\"\])|(\[_\])|(\.\w+))*` //(\.\w+)*` // (\.\w+\[\"\w+\"\])|(\.\w+\[\w+\])|(\.\w+))*`
// input_extract_pattern = `input\.parameters((\[\".+\"\])?(\.\w+\[\"\w+\"\])|(\.\w+\[\w+\])|(\.\w+))+`
obj_get_pattern = `object\.get\((input|input\.parameters|input\.parameters\.[^,]+)\, \"*([^,\"]+)\"*, [^\)]+\)`
obj_get_pattern = `object\.get\((input|input\.parameters|input\.parameters\.[^,]+)\, \"*([^,\"]+)\"*, [^\)]+\)`
package_name_regex = `package ([\w\.]+)[\n\r]+`
import_regex = `import ([\w\.]+)[\n\r]+`
)

var (
Expand Down Expand Up @@ -268,6 +271,23 @@ func ExtractParameter(modules map[string]*ast.Module) []*domain.ParameterDef {
return defStore.store
}

func MergeRegoAndLibs(rego string, libs []string) string {
if len(libs) == 0 {
return rego
}

var re = regexp.MustCompile(import_regex)
var re2 = regexp.MustCompile(package_name_regex)

result := re.ReplaceAllString(rego, "")

for _, lib := range libs {
result += re2.ReplaceAllString(lib, "")
}

return result
}

type ParamDefStore struct {
store []*domain.ParameterDef
}
Expand Down Expand Up @@ -346,3 +366,75 @@ func createKey(key string, isLast bool) *domain.ParameterDef {

return newDef
}

func CompileRegoWithLibs(rego string, libs []string) (compiler *ast.Compiler, err error) {
modules := map[string]*ast.Module{}

regoPackage := GetPackageFromRegoCode(rego)

regoModule, err := ast.ParseModuleWithOpts(regoPackage, rego, ast.ParserOptions{})
if err != nil {
return nil, err
}

modules[regoPackage] = regoModule

for i, lib := range libs {
// Lib이 공백이면 무시
if len(strings.TrimSpace(lib)) == 0 {
continue
}

libPackage := GetPackageFromRegoCode(lib)

// Lib의 패키지 명이 공백이면 rego에서 import 될 수 없기 때문에 에러 처리
// 패키지 명이 Parse할 때 비어있으면 에러가 나지만, rego인지 lib인지 정확히 알기 어려울 수 있으므로 알려 줌
if len(strings.TrimSpace(libPackage)) == 0 {
return nil, fmt.Errorf("lib[%d] is not valid, empty package name", i)
}

libModule, err := ast.ParseModuleWithOpts(libPackage, lib, ast.ParserOptions{})
if err != nil {
return nil, err
}

modules[libPackage] = libModule
}

compiler = ast.NewCompiler()
compiler.Compile(modules)

return compiler, nil
}

func MergeAndCompileRegoWithLibs(rego string, libs []string) (modules map[string]*ast.Module, err error) {
modules = map[string]*ast.Module{}

regoPackage := GetPackageFromRegoCode(rego)

merged := MergeRegoAndLibs(rego, libs)

module, err := ast.ParseModuleWithOpts(regoPackage, merged, ast.ParserOptions{})
if err != nil {
return modules, err
}

modules[regoPackage] = module

compiler := ast.NewCompiler()
compiler.Compile(modules)

return modules, nil
}

func GetPackageFromRegoCode(regoCode string) string {
packageRegex := regexp.MustCompile(package_name_regex)

match := packageRegex.FindStringSubmatch(regoCode)

if len(match) > 1 {
return match[1]
}

return ""
}
92 changes: 92 additions & 0 deletions internal/policy-template/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package policytemplate

import (
"encoding/json"
"fmt"
"slices"
"strings"

"github.com/openinfradev/tks-api/pkg/domain"
"github.com/xeipuuv/gojsonschema"
)

var VALID_PARAM_TYPES = []string{"string", "number", "integer", "object", "boolean", "null"}

func ValidateParamDef(paramdef *domain.ParameterDef) error {
paramType := paramdef.Type

baseType := strings.TrimSuffix(paramType, "[]")

// 타입과 []를 제거한 타입이 다르면 array인데 이것과 isArray 속성이 다르면 에러임
if (baseType != paramType) != paramdef.IsArray {
return fmt.Errorf("type is '%s', but IsArray=%v", paramType, paramdef.IsArray)
}

if slices.Contains(VALID_PARAM_TYPES, baseType) {
return nil
}

return fmt.Errorf("%s is not valid type", paramType)
}

func ValidateParamDefs(paramdefs []*domain.ParameterDef) error {
for _, paramdef := range paramdefs {
err := ValidateParamDef(paramdef)
if err != nil {
return err
}

err = ValidateParamDefs(paramdef.Children)

if err != nil {
return err
}
}

return nil
}

func ValidateJSONusingParamdefs(paramdefs []*domain.ParameterDef, jsonStr string) error {
jsonSchema := ParamDefsToJSONSchemaProeprties(paramdefs)

if jsonSchema == nil {
// 파라미터가 없는데 "{}" 이나 ""이면 에러가 아님
if isEmptyValue(jsonStr) {
return nil
} else {
return fmt.Errorf("schema has no field but value '%v' specified", jsonStr)
}
}

// Load JSON Schema
schemaLoader := gojsonschema.NewGoLoader(jsonSchema)

// Load JSON data
documentLoader := gojsonschema.NewStringLoader(jsonStr)

// Validate JSON against JSON Schema
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return err
}

// Check if the result is valid
if result.Valid() {
return nil
}

schemaBytes, _ := json.Marshal(paramdefs)
jsonSchemaBytes, _ := json.Marshal(jsonSchema)

return fmt.Errorf("value '%s' is not valid against schemas:\njsonschema='%s',\nparamdefs='%s'",
jsonStr, string(jsonSchemaBytes), string(schemaBytes))
}

func isEmptyValue(value string) bool {
val := strings.TrimSpace(value)
val = strings.TrimPrefix(val, "{")
val = strings.TrimSuffix(val, "}")
val = strings.TrimSpace(val)

return len(val) == 0
}
82 changes: 58 additions & 24 deletions internal/usecase/policy-template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

mapset "github.com/deckarep/golang-set/v2"
"github.com/google/uuid"
"github.com/open-policy-agent/opa/ast"
"github.com/openinfradev/tks-api/internal/middleware/auth/request"
"github.com/openinfradev/tks-api/internal/model"
"github.com/openinfradev/tks-api/internal/pagination"
Expand Down Expand Up @@ -95,6 +94,10 @@ func (u *PolicyTemplateUsecase) Create(ctx context.Context, dto model.PolicyTemp
}
}

if err := policytemplate.ValidateParamDefs(dto.ParametersSchema); err != nil {
return uuid.Nil, httpErrors.NewBadRequestError(err, "PT_INVALID_PARAMETER_SCHEMA", "")
}

if dto.IsTksTemplate() {
// TKS 템블릿이면
dto.Mandatory = false
Expand Down Expand Up @@ -431,25 +434,27 @@ func (u *PolicyTemplateUsecase) CreatePolicyTemplateVersion(ctx context.Context,
"PT_NOT_PERMITTED_ON_TKS_POLICY_TEMPLATE", "")
}

if err := policytemplate.ValidateParamDefs(policyTemplate.ParametersSchema); err != nil {
return "", httpErrors.NewBadRequestError(err, "PT_INVALID_PARAMETER_SCHEMA", "")
}

return u.repo.CreatePolicyTemplateVersion(ctx, policyTemplateId, newVersion, schema, rego, libs)
}

func (u *PolicyTemplateUsecase) RegoCompile(request *domain.RegoCompileRequest, parseParameter bool) (response *domain.RegoCompileResponse, err error) {
modules := map[string]*ast.Module{}

response = &domain.RegoCompileResponse{}
response.Errors = []domain.RegoCompieError{}

mod, err := ast.ParseModuleWithOpts("rego", request.Rego, ast.ParserOptions{})
if err != nil {
return nil, err
}
modules["rego"] = mod

compiler := ast.NewCompiler()
compiler.Compile(modules)
compiler, err := policytemplate.CompileRegoWithLibs(request.Rego, request.Libs)

if compiler.Failed() {
if err != nil {
response.Errors = append(response.Errors, domain.RegoCompieError{
Status: 400,
Code: "PT_FAILED_TO_LOAD_REGO_MODULE",
Message: "failed to load rego module",
Text: err.Error(),
})
} else if compiler.Failed() {
for _, compileError := range compiler.Errors {
response.Errors = append(response.Errors, domain.RegoCompieError{
Status: 400,
Expand All @@ -462,8 +467,23 @@ func (u *PolicyTemplateUsecase) RegoCompile(request *domain.RegoCompileRequest,
}
}

if len(response.Errors) > 0 {
return response, nil
}

if parseParameter {
response.ParametersSchema = policytemplate.ExtractParameter(modules)
// 효율적인 파라미터 추출을 위한 머지
modules, err := policytemplate.MergeAndCompileRegoWithLibs(request.Rego, request.Libs)
if err != nil {
response.Errors = append(response.Errors, domain.RegoCompieError{
Status: 400,
Code: "PT_FAILED_TO_LOAD_REGO_MODULE",
Message: "failed to load merged rego module",
Text: err.Error(),
})
} else {
response.ParametersSchema = policytemplate.ExtractParameter(modules)
}
}

return response, nil
Expand Down Expand Up @@ -549,21 +569,20 @@ func (u *PolicyTemplateUsecase) ExtractPolicyParameters(ctx context.Context, org
}
}

modules := map[string]*ast.Module{}

response = &domain.RegoCompileResponse{}
response.Errors = []domain.RegoCompieError{}

mod, err := ast.ParseModuleWithOpts("rego", rego, ast.ParserOptions{})
if err != nil {
return nil, err
}
modules["rego"] = mod

compiler := ast.NewCompiler()
compiler.Compile(modules)
compiler, err := policytemplate.CompileRegoWithLibs(rego, libs)

if compiler.Failed() {
if err != nil {
response.Errors = append(response.Errors, domain.RegoCompieError{
Status: 400,
Code: "PT_FAILED_TO_LOAD_REGO_MODULE",
Message: "failed to load rego module",
Text: err.Error(),
})
return response, nil
} else if compiler.Failed() {
for _, compileError := range compiler.Errors {
response.Errors = append(response.Errors, domain.RegoCompieError{
Status: 400,
Expand All @@ -574,6 +593,21 @@ func (u *PolicyTemplateUsecase) ExtractPolicyParameters(ctx context.Context, org
compileError.Message),
})
}
return response, nil
}

if len(response.Errors) > 0 {
return response, nil
}

modules, err := policytemplate.MergeAndCompileRegoWithLibs(rego, libs)
if err != nil {
response.Errors = append(response.Errors, domain.RegoCompieError{
Status: 400,
Code: "PT_FAILED_TO_LOAD_REGO_MODULE",
Message: "failed to load merged rego module",
Text: err.Error(),
})
}

extractedParamDefs := policytemplate.ExtractParameter(modules)
Expand Down
Loading

0 comments on commit 5b8a434

Please sign in to comment.