pkg/gcv/configs/config.go (443 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // configs helps with loading and parsing configuration files package configs import ( "context" "fmt" "regexp" "sort" "strings" "github.com/golang/glog" "github.com/GoogleCloudPlatform/config-validator/pkg/multierror" cfapis "github.com/open-policy-agent/frameworks/constraint/pkg/apis" cftemplates "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/regorewriter" "github.com/open-policy-agent/opa/ast" "github.com/pkg/errors" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/kubectl/pkg/scheme" ) func init() { utilruntime.Must(cfapis.AddToScheme(scheme.Scheme)) utilruntime.Must(apiextensions.AddToScheme(scheme.Scheme)) utilruntime.Must(apiextensionsv1beta1.AddToScheme(scheme.Scheme)) } // TODO: Using constant from gcptarget/tftarget packages causes circular reference. Fix circular reference and use <package>.Name const ( K8STargetName = "admission.k8s.gatekeeper.sh" GCPTargetName = "validation.gcp.forsetisecurity.org" TFTargetName = "validation.resourcechange.terraform.cloud.google.com" ) const ( constraintGroup = "constraints.gatekeeper.sh" templateGroup = "templates.gatekeeper.sh" yamlPath = GCPTargetName + "/yamlpath" OriginalName = GCPTargetName + "/originalName" ) const ( gcpConstraint = "gcp" k8sConstraint = "k8s" tfConstraint = "terraform" ) func setAnnotation(u *unstructured.Unstructured, key, value string) { annotations := u.GetAnnotations() if annotations == nil { annotations = map[string]string{} } annotations[key] = value u.SetAnnotations(annotations) } // PolicyFile represents a .yaml file with its path and contents, // which may or may not have been loaded from the file system. type PolicyFile struct { Path string Content []byte } // LoadUnstructured loads .yaml files from the provided directories as k8s // unstructured.Unstructured types. func LoadUnstructured(dirs []string) ([]*unstructured.Unstructured, error) { var files []*PolicyFile for _, dir := range dirs { dirPath, err := NewPath(dir) if err != nil { return nil, err } dirFiles, err := dirPath.ReadAll(context.Background(), SuffixPredicate(".yaml")) if err != nil { return nil, err } for _, dirFile := range dirFiles { files = append(files, &PolicyFile{ Path: dirFile.Path, Content: dirFile.Content, }) } } yamlDocs, err := LoadUnstructuredFromContents(files) if err != nil { return nil, err } if len(yamlDocs) == 0 { return nil, fmt.Errorf("zero configurations found in the provided directories: %v", dirs) } return yamlDocs, nil } // LoadUnstructuredFromContents loads provided file contents as k8s unstructured.Unstructured types. func LoadUnstructuredFromContents(files []*PolicyFile) ([]*unstructured.Unstructured, error) { var yamlDocs []*unstructured.Unstructured for _, file := range files { documents := strings.Split(string(file.Content), "\n---") for _, rawDoc := range documents { document := strings.TrimLeft(rawDoc, "\n ") if len(document) == 0 { continue } var u unstructured.Unstructured _, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(document), nil, &u) if err != nil { return nil, errors.Wrapf(err, "failed to decode %s", file.Path) } setAnnotation(&u, yamlPath, file.Path) yamlDocs = append(yamlDocs, &u) } } return yamlDocs, nil } const regoAdapter = ` violation[{"msg": message, "details": metadata}] { deny[{"msg": message, "details": metadata}] with input as {"asset": input.review, "constraint": {"spec": {"parameters": input.parameters}}} } ` func injectRegoAdapter(rego string) string { return rego + "\n" + regoAdapter } // convertLegacyConstraintTemplate handles converting a legacy forseti v1alpha1 ConstraintTemplate // to a constraint framework v1alpha1 ConstraintTemplate. // Legacy constraint templates use `deny` as an entrypoint and the expected inputs are: // - `input.asset`: the CAI asset being reviewed (new templates use `input.review`) // - `input.constraint.spec.parameters`: the parameters from the constraint template (new templates use `input.parameters`) func convertLegacyConstraintTemplate(u *unstructured.Unstructured, regoLib []string) error { targetMap, found, err := unstructured.NestedMap(u.Object, "spec", "targets") if err != nil && !found { return nil } if u.GroupVersionKind().Version != "v1alpha1" { return errors.Errorf("only v1alpha1 constraint templates are eligible for legacy conversion") } // Make name match kind as appropriate ctKind, found, err := unstructured.NestedString(u.Object, "spec", "crd", "spec", "names", "kind") if err != nil { return errors.Wrapf(err, "invalid kind at spec.crd.spec.names.kind") } if !found { return errors.Errorf("No kind found at spec.crd.spec.names.kind") } if len(targetMap) != 1 { return errors.Errorf("got invalid number of targets %d", len(targetMap)) } // Transcode target var targets []interface{} for name, targetIface := range targetMap { legacyTarget, ok := targetIface.(map[string]interface{}) if !ok { return errors.Errorf("wrong type in legacy target") } target := map[string]interface{}{} regoIface, found := legacyTarget["rego"] if !found { return errors.Errorf("no rego specified in template") } rego, ok := regoIface.(string) if !ok { return errors.Errorf("failed to get rego from template") } rr, err := regorewriter.New(regorewriter.NewPackagePrefixer("lib"), []string{"data.validator"}, nil) if err != nil { return errors.Wrapf(err, "failed to create rego rewriter") } for idx, lib := range regoLib { path := fmt.Sprintf("idx-%d.rego", idx) m, err := ast.ParseModule(path, lib) if err != nil { return fmt.Errorf("failed to ParseModule with path %s: %w", path, err) } if err := rr.AddLib(path, m); err != nil { return errors.Wrapf(err, "failed to add lib %d", idx) } } path := "template-rego" m, err := ast.ParseModule(path, injectRegoAdapter(rego)) if err != nil { return fmt.Errorf("failed to ParseModule with path %s: %w", path, err) } if err := rr.AddEntryPoint(path, m); err != nil { return errors.Wrapf(err, "failed to add source") } srcs, err := rr.Rewrite() if err != nil { return errors.Wrapf(err, "failed to rewrite") } if len(srcs.EntryPoints) != 1 { return errors.Errorf("invalid number of entrypoints") } newRego, err := srcs.EntryPoints[0].Content() if err != nil { return errors.Wrapf(err, "failed to convert rego to bytes") } var libs []interface{} for _, lib := range srcs.Libs { libBytes, err := lib.Content() if err != nil { return errors.Wrapf(err, "failed to convert lib to bytes") } libs = append(libs, string(libBytes)) } target["rego"] = string(newRego) target["libs"] = libs target["target"] = name targets = append(targets, target) } if err := unstructured.SetNestedSlice(u.Object, targets, "spec", "targets"); err != nil { return errors.Wrapf(err, "failed to set transcoded target spec") } originalName := u.GetName() u.SetName(strings.ToLower(ctKind)) setAnnotation(u, OriginalName, originalName) return nil } var terminatingStarRegex = regexp.MustCompilePOSIX(`/\*$`) var starRegex = regexp.MustCompilePOSIX(`/\*/`) func fixLegacyMatcher(ancestry string) string { normalized := NormalizeAncestry(ancestry) return starRegex.ReplaceAllString( terminatingStarRegex.ReplaceAllString(normalized, "/**"), "/**/", ) } func NormalizeAncestry(val string) string { for _, r := range []struct { old string new string }{ {"organization/", "organizations/"}, {"folder/", "folders/"}, {"project/", "projects/"}, } { val = strings.ReplaceAll(val, r.old, r.new) } return val } func convertLegacyResourceName(u *unstructured.Unstructured) { originalName := u.GetName() compatibleName := strings.ReplaceAll(strings.ToLower(originalName), "_", "-") if originalName == compatibleName { return } u.SetName(compatibleName) setAnnotation(u, OriginalName, originalName) } func convertLegacyCRM(obj map[string]interface{}, field ...string) error { strs, found, err := unstructured.NestedStringSlice(obj, field...) if err != nil { return errors.Wrapf(err, "invalid field type for %s", field) } if !found { return nil } for idx, val := range strs { strs[idx] = fixLegacyMatcher(val) } return unstructured.SetNestedStringSlice(obj, strs, field...) } func convertLegacyConstraint(u *unstructured.Unstructured) error { convertLegacyResourceName(u) if err := convertLegacyCRM(u.Object, "spec", "match", "target"); err != nil { return err } if err := convertLegacyCRM(u.Object, "spec", "match", "exclude"); err != nil { return err } return nil } // Configuration represents the configuration files fed into FCV. type Configuration struct { GCPTemplates []*cftemplates.ConstraintTemplate // Constraint Templates for GCP GCPConstraints []*unstructured.Unstructured // Constraints for GCP K8STemplates []*cftemplates.ConstraintTemplate // Constraint Templates for GKE K8SConstraints []*unstructured.Unstructured // Constraints for GKE TFTemplates []*cftemplates.ConstraintTemplate // Constraint Templates for TF TFConstraints []*unstructured.Unstructured // Constraints for TF // regoLib contains the set of rego libraries, it is only used during construction of Configuration regoLib []string // allConstraints contains all input constraints, it is only used during construction of Configuration allConstraints []*unstructured.Unstructured // templateNames is a set of the names of all templates for checking exclusivity. templateNames map[string]*cftemplates.ConstraintTemplate // templateNames is a set of the kinds of all templates for checking exclusivity. templateKinds map[string]*cftemplates.ConstraintTemplate } func newConfiguration() *Configuration { return &Configuration{ templateNames: map[string]*cftemplates.ConstraintTemplate{}, templateKinds: map[string]*cftemplates.ConstraintTemplate{}, } } // LoadRegoFiles load rego policy library files from the given directory. func LoadRegoFiles(dir string) ([]string, error) { dirPath, err := NewPath(dir) if err != nil { return nil, errors.Wrapf(err, "failed to handle path for %s", dir) } files, err := dirPath.ReadAll(context.Background(), SuffixPredicate(".rego")) if err != nil { return nil, errors.Wrapf(err, "failed to read files from %s", dir) } var libs []string for _, f := range files { libs = append(libs, string(f.Content)) } sort.Strings(libs) return libs, nil } func (c *Configuration) loadUnstructured(u *unstructured.Unstructured) error { switch u.GroupVersionKind().Group { case constraintGroup: if u.GroupVersionKind().Version == "v1alpha1" { glog.Warning( "v1alpha1 constraints are deprecated and will be removed in a future release. " + "Please upgrade: https://github.com/GoogleCloudPlatform/policy-library/blob/main/docs/constraint_template_authoring.md#updating-from-v1alpha1-templates", ) } c.allConstraints = append(c.allConstraints, u) case templateGroup: if u.GroupVersionKind().Kind != "ConstraintTemplate" { return errors.Errorf("unexpected data type %s in group %s", u.GroupVersionKind(), templateGroup) } switch u.GroupVersionKind().Version { case "v1alpha1": glog.Warning( "v1alpha1 constraint templates are deprecated and will be removed in a future release. " + "Please upgrade: https://github.com/GoogleCloudPlatform/policy-library/blob/main/docs/constraint_template_authoring.md#updating-from-v1alpha1-templates", ) openAPIResult := configValidatorV1Alpha1SchemaValidator.Validate(u.Object) if openAPIResult.HasErrorsOrWarnings() { return errors.Wrapf(openAPIResult.AsError(), "v1alpha1 validation failure") } if err := convertLegacyConstraintTemplate(u, c.regoLib); err != nil { return errors.Wrapf(err, "failed to convert legacy forseti ConstraintTemplate "+ "to ConstraintFramework format, this is likely due to an issue in the spec.crd.spec.validation field") } case "v1beta1": openAPIResult := configValidatorV1Beta1SchemaValidator.Validate(u.Object) if openAPIResult.HasErrorsOrWarnings() { return errors.Wrapf(openAPIResult.AsError(), "v1beta1 validation failure") } default: return errors.Errorf("unrecognized ConstraintTemplate version %s", u.GroupVersionKind().Version) } groupVersioner := runtime.GroupVersioner(schema.GroupVersions(scheme.Scheme.PrioritizedVersionsAllGroups())) obj, err := scheme.Scheme.ConvertToVersion(u, groupVersioner) if err != nil { return errors.Wrapf(err, "failed to convert unstructured ConstraintTemplate to versioned") } var ct cftemplates.ConstraintTemplate if err := scheme.Scheme.Convert(obj, &ct, nil); err != nil { return errors.Wrapf(err, "failed to convert to versioned constraint template internal struct") } if ct.Spec.CRD.Spec.Validation.OpenAPIV3Schema.Type == "" { glog.Warning( "spec.crd.spec.validation.openAPIV3Schema is missing the type: declaration. " + "Please upgrade: https://open-policy-agent.github.io/gatekeeper/website/docs/constrainttemplates#v1-constraint-template", ) ct.Spec.CRD.Spec.Validation.OpenAPIV3Schema.Type = "object" } if dup, found := c.templateNames[ct.Name]; found { return errors.Errorf( "ConstraintTemplate %q declared at path %q has duplicate name conflict with template declared at path %q", ct.Name, ct.GetAnnotations()[yamlPath], dup.GetAnnotations()[yamlPath]) } c.templateNames[ct.Name] = &ct if dup, found := c.templateKinds[ct.Name]; found { return errors.Errorf( "ConstraintTemplate %q crd kind %q declared at path %q has duplicate kind conflict with template declared at path %q", ct.Name, ct.Spec.CRD.Spec.Names.Kind, ct.GetAnnotations()[yamlPath], dup.GetAnnotations()[yamlPath]) } c.templateKinds[ct.Name] = &ct for _, target := range ct.Spec.Targets { switch target.Target { case GCPTargetName: c.GCPTemplates = append(c.GCPTemplates, &ct) case TFTargetName: if u.GroupVersionKind().Version == "v1alpha1" { return errors.Errorf("v1alpha1 templates are not supported for terraform templates. Please upgrade.") } c.TFTemplates = append(c.TFTemplates, &ct) case K8STargetName: c.K8STemplates = append(c.K8STemplates, &ct) default: return errors.Errorf("") } } default: glog.V(1).Infof("Ignoring %s %s", u.GroupVersionKind(), u.GetName()) } return nil } func (c *Configuration) finishLoad() error { templates := map[string]string{} for _, t := range c.GCPTemplates { templates[t.Spec.CRD.Spec.Names.Kind] = gcpConstraint } for _, t := range c.TFTemplates { templates[t.Spec.CRD.Spec.Names.Kind] = tfConstraint } for _, t := range c.K8STemplates { templates[t.Spec.CRD.Spec.Names.Kind] = k8sConstraint } byTemplate := map[string]map[string]*unstructured.Unstructured{} allConstraints := c.allConstraints c.allConstraints = nil for _, constraint := range allConstraints { gvk := constraint.GroupVersionKind() if gvk.Version == "v1alpha1" { if err := convertLegacyConstraint(constraint); err != nil { return fmt.Errorf("failed to convert constraint: %w", err) } } templateConstraints, found := byTemplate[constraint.GetKind()] if !found { templateConstraints = map[string]*unstructured.Unstructured{} byTemplate[constraint.GetKind()] = templateConstraints } if dup, found := templateConstraints[constraint.GetName()]; found { return errors.Errorf( "Constraint %q declared at path %q has duplicate name conflict with constraint declared at path %q", dup.GetName(), dup.GetAnnotations()[yamlPath], constraint.GetAnnotations()[yamlPath]) } switch templates[gvk.Kind] { case gcpConstraint: c.GCPConstraints = append(c.GCPConstraints, constraint) case tfConstraint: c.TFConstraints = append(c.TFConstraints, constraint) case k8sConstraint: c.K8SConstraints = append(c.K8SConstraints, constraint) default: return errors.Errorf("constraint %s does not correspond to any templates", gvk) } } return nil } // NewConfiguration returns the configuration from the list of provided directories. func NewConfiguration(dirs []string, libDir string) (*Configuration, error) { unstructuredObjects, err := LoadUnstructured(dirs) if err != nil { return nil, err } regoLib, err := LoadRegoFiles(libDir) if err != nil { return nil, err } return NewConfigurationFromContents(unstructuredObjects, regoLib) } // NewConfigurationFromContents returns the configuration from the given // unstructured objects and the rego library file contents. // This can be used by code that may not have access to a file system and passes in the contents directly. func NewConfigurationFromContents(unstructuredObjects []*unstructured.Unstructured, regoLib []string) (*Configuration, error) { configuration := newConfiguration() configuration.regoLib = regoLib var errs multierror.Errors for _, u := range unstructuredObjects { if err := configuration.loadUnstructured(u); err != nil { yamlPath := u.GetAnnotations()[yamlPath] name := u.GetName() errs.Add(errors.Wrapf(err, "failed to load resource %s %s", yamlPath, name)) } } if !errs.Empty() { return nil, errs.ToError() } if err := configuration.finishLoad(); err != nil { return nil, errors.Wrapf(err, "config error") } return configuration, nil }