pkg/gcv/validator.go (219 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.
// Package gcv provides a library and a RPC service for Forseti Config Validator.
package gcv
import (
"context"
"encoding/json"
"fmt"
"github.com/GoogleCloudPlatform/config-validator/pkg/api/validator"
asset2 "github.com/GoogleCloudPlatform/config-validator/pkg/asset"
"github.com/GoogleCloudPlatform/config-validator/pkg/gcptarget"
"github.com/GoogleCloudPlatform/config-validator/pkg/gcv/configs"
"github.com/GoogleCloudPlatform/config-validator/pkg/multierror"
"github.com/GoogleCloudPlatform/config-validator/pkg/tftarget"
"github.com/golang/glog"
cfclient "github.com/open-policy-agent/frameworks/constraint/pkg/client"
"github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego"
cftemplates "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates"
"github.com/open-policy-agent/frameworks/constraint/pkg/handler"
k8starget "github.com/open-policy-agent/gatekeeper/v3/pkg/target"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
logRequestsVerboseLevel = 2
// The JSON object key for ancestry path
ancestryPathKey = "ancestry_path"
// The JSON object key for ancestors list
ancestorSliceKey = "ancestors"
)
type ConfigValidator interface {
ReviewAsset(ctx context.Context, asset *validator.Asset) ([]*validator.Violation, error)
}
// Validator checks GCP resource metadata for constraint violation.
//
// Expected usage pattern:
// - call NewValidator to create a new Validator
// - call AddData one or more times to add the GCP resource metadata to check
// - call Audit to validate the GCP resource metadata that has been added so far
// - call Reset to delete existing data
// - call AddData to add a new set of GCP resource metadata to check
// - call Reset to delete existing data
//
// Any data added in AddData stays in the underlying rule evaluation engine's memory.
// To avoid out of memory errors, callers can invoke Reset to delete existing data.
type Validator struct {
gcpCFClient *cfclient.Client
k8sCFClient *cfclient.Client
tfCFClient *cfclient.Client
}
// Stores functional options for CF client
type initOptions struct {
driverArgs []rego.Arg
clientArgs []cfclient.Opt
}
type Option = func(*initOptions)
func DisableBuiltins(builtins ...string) Option {
return func(o *initOptions) {
o.driverArgs = append(o.driverArgs, rego.DisableBuiltins(builtins...))
}
}
// NewValidatorConfig returns a new ValidatorConfig.
// By default it will initialize the underlying query evaluation engine by loading supporting library, constraints, and constraint templates.
// We may want to make this initialization behavior configurable in the future.
func NewValidatorConfig(policyPaths []string, policyLibraryPath string) (*configs.Configuration, error) {
if len(policyPaths) == 0 {
return nil, fmt.Errorf("No policy path set, provide an option to set the policy path gcv.PolicyPath")
}
if policyLibraryPath == "" {
return nil, fmt.Errorf("No policy library set")
}
glog.V(logRequestsVerboseLevel).Infof("loading policy dir: %v lib dir: %s", policyPaths, policyLibraryPath)
return configs.NewConfiguration(policyPaths, policyLibraryPath)
}
func newCFClient(
targetHandler handler.TargetHandler,
templates []*cftemplates.ConstraintTemplate,
constraints []*unstructured.Unstructured,
opts ...Option) (
*cfclient.Client, error) {
options := &initOptions{
driverArgs: []rego.Arg{rego.Tracing(false)},
clientArgs: []cfclient.Opt{cfclient.Targets(targetHandler)},
}
for _, opt := range opts {
opt(options)
}
driver, err := rego.New(options.driverArgs...)
if err != nil {
return nil, fmt.Errorf("unable to create new driver: %w", err)
}
// Append driver option after creation
args := append(options.clientArgs, cfclient.Driver(driver))
cfClient, err := cfclient.NewClient(args...)
if err != nil {
return nil, fmt.Errorf("unable to set up Constraint Framework client: %w", err)
}
ctx := context.Background()
var errs multierror.Errors
for _, template := range templates {
if _, err := cfClient.AddTemplate(ctx, template); err != nil {
errs.Add(fmt.Errorf("failed to add template %v: %w", template, err))
}
}
if !errs.Empty() {
return nil, errs.ToError()
}
for _, constraint := range constraints {
if _, err := cfClient.AddConstraint(ctx, constraint); err != nil {
errs.Add(fmt.Errorf("failed to add constraint %s: %w", constraint, err))
}
}
if !errs.Empty() {
return nil, errs.ToError()
}
return cfClient, nil
}
// NewValidatorFromConfig creates the validator from a config.
func NewValidatorFromConfig(config *configs.Configuration, opts ...Option) (*Validator, error) {
gcpCFClient, err := newCFClient(gcptarget.New(), config.GCPTemplates, config.GCPConstraints, opts...)
if err != nil {
return nil, fmt.Errorf("unable to set up GCP Constraint Framework client: %w", err)
}
k8sCFClient, err := newCFClient(&k8starget.K8sValidationTarget{}, config.K8STemplates, config.K8SConstraints, opts...)
if err != nil {
return nil, fmt.Errorf("unable to set up K8S Constraint Framework client: %w", err)
}
tfCFClient, err := newCFClient(tftarget.New(), config.TFTemplates, config.TFConstraints, opts...)
if err != nil {
return nil, fmt.Errorf("unable to set up TF Constraint Framework client: %w", err)
}
ret := &Validator{
gcpCFClient: gcpCFClient,
k8sCFClient: k8sCFClient,
tfCFClient: tfCFClient,
}
return ret, nil
}
// NewValidator returns a new Validator.
// By default it will initialize the underlying query evaluation engine by loading supporting library, constraints, and constraint templates.
// We may want to make this initialization behavior configurable in the future.
func NewValidator(policyPaths []string, policyLibraryPath string, opts ...Option) (*Validator, error) {
config, err := NewValidatorConfig(policyPaths, policyLibraryPath)
if err != nil {
return nil, err
}
return NewValidatorFromConfig(config, opts...)
}
// NewValidatorFromContents returns a new Validator built from the provided contents of the policy constraints and policy library.
// This provides a way to create a validator directly from contents instead of reading from the file system.
// policyLibrary is a slice of file contents of all policy library files.
func NewValidatorFromContents(policyFiles []*configs.PolicyFile, policyLibrary []string, opts ...Option) (*Validator, error) {
if len(policyFiles) == 0 {
return nil, fmt.Errorf("No policy constraints provided")
}
if len(policyLibrary) == 0 {
return nil, fmt.Errorf("No policy library provided")
}
unstructuredObjects, err := configs.LoadUnstructuredFromContents(policyFiles)
if err != nil {
return nil, err
}
config, err := configs.NewConfigurationFromContents(unstructuredObjects, policyLibrary)
if err != nil {
return nil, err
}
return NewValidatorFromConfig(config, opts...)
}
// ReviewAsset reviews a single asset.
func (v *Validator) ReviewAsset(ctx context.Context, asset *validator.Asset) ([]*validator.Violation, error) {
// Sanitize the ancestry path first, so that an asset that only provides ancestors
// can still pass ValidateAsset.
if err := asset2.SanitizeAncestryPath(asset); err != nil {
return nil, err
}
if err := asset2.ValidateAsset(asset); err != nil {
return nil, err
}
assetInterface, err := asset2.ConvertResourceViaJSONToInterface(asset)
if err != nil {
return nil, err
}
assetMapInterface := assetInterface.(map[string]interface{})
result, err := v.ReviewUnmarshalledJSON(ctx, assetMapInterface)
if err != nil {
return nil, err
}
return result.ToViolations()
}
// ReviewTFResourceChange evaluates a single terraform resource change without any threading in the background.
func (v *Validator) ReviewTFResourceChange(ctx context.Context, inputResource map[string]interface{}) ([]*validator.Violation, error) {
target := tftarget.New()
handled, _, err := target.HandleReview(inputResource)
if !handled {
return nil, fmt.Errorf("Unhandled resource: %w", err)
}
responses, err := v.tfCFClient.Review(ctx, inputResource)
if err != nil {
return nil, fmt.Errorf("TF target Constraint Framework review call failed: %w", err)
}
result, err := NewResult(tftarget.Name, inputResource["address"].(string), inputResource, inputResource, responses)
if err != nil {
return nil, err
}
return result.ToViolations()
}
// fixAncestry will try to use the ancestors array to create the ancestorPath
// value if it is not present.
func (v *Validator) fixAncestry(input map[string]interface{}) error {
ancestors, found, err := unstructured.NestedStringSlice(input, ancestorSliceKey)
if found && err == nil {
input[ancestryPathKey] = asset2.AncestryPath(ancestors)
return nil
}
ancestry, found, err := unstructured.NestedString(input, ancestryPathKey)
if found && err == nil {
input[ancestryPathKey] = configs.NormalizeAncestry(ancestry)
return nil
}
return fmt.Errorf("asset missing ancestry information: %v", input)
}
// ReviewJSON reviews the content of a JSON string
func (v *Validator) ReviewJSON(ctx context.Context, data string) (*Result, error) {
asset := map[string]interface{}{}
if err := json.Unmarshal([]byte(data), &asset); err != nil {
return nil, fmt.Errorf("failed to unmarshal json: %w", err)
}
return v.ReviewUnmarshalledJSON(ctx, asset)
}
// ReviewJSON evaluates a single asset without any threading in the background.
func (v *Validator) ReviewUnmarshalledJSON(ctx context.Context, asset map[string]interface{}) (*Result, error) {
if err := v.fixAncestry(asset); err != nil {
return nil, err
}
if asset2.IsK8S(asset) {
return v.reviewK8SResource(ctx, asset)
}
return v.reviewGCPResource(ctx, asset)
}
// reviewK8SResource will convert CAI assets to k8s resources then pass them to the cf client with the gatekeeper target.
func (v *Validator) reviewK8SResource(ctx context.Context, asset map[string]interface{}) (*Result, error) {
k8sResource, err := asset2.ConvertCAIToK8s(asset)
if err != nil {
return nil, fmt.Errorf("failed to convert asset to admission request: %w", err)
}
responses, err := v.k8sCFClient.Review(ctx, k8sResource)
if err != nil {
return nil, fmt.Errorf("K8S target Constraint Framework review call failed: %w", err)
}
return NewResult(configs.K8STargetName, asset["name"].(string), asset, k8sResource.Object, responses)
}
// reviewGCPResource will pass CAI assets to the cf client with the GCP target.
func (v *Validator) reviewGCPResource(ctx context.Context, asset map[string]interface{}) (*Result, error) {
responses, err := v.gcpCFClient.Review(ctx, asset)
if err != nil {
return nil, fmt.Errorf("GCP target Constraint Framework review call failed: %w", err)
}
return NewResult(gcptarget.Name, asset["name"].(string), asset, asset, responses)
}