pkg/asset/asset.go (157 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 asset
import (
"encoding/json"
"regexp"
"strings"
"github.com/GoogleCloudPlatform/config-validator/pkg/api/validator"
"github.com/GoogleCloudPlatform/config-validator/pkg/gcv/configs"
"github.com/golang/glog"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const logRequestsVerboseLevel = 2
func ValidateAsset(asset *validator.Asset) error {
var result *multierror.Error
if asset.GetName() == "" {
result = multierror.Append(result, errors.New("missing asset name"))
}
if asset.GetAncestryPath() == "" {
result = multierror.Append(result, errors.Errorf("asset %q missing ancestry path", asset.GetName()))
}
if asset.GetAssetType() == "" {
result = multierror.Append(result, errors.Errorf("asset %q missing type", asset.GetName()))
}
if asset.GetResource() == nil && asset.GetIamPolicy() == nil && asset.GetOrgPolicy() == nil && asset.GetAccessContextPolicy() == nil && asset.GetV2OrgPolicies() == nil {
result = multierror.Append(result, errors.Errorf("asset %q missing all of these: resource, IAM policy, Org Policy, Access Context Policy, v2 Org Policy", asset.GetName()))
}
return result.ErrorOrNil()
}
func ConvertResourceViaJSONToInterface(asset *validator.Asset) (interface{}, error) {
if asset == nil {
return nil, nil
}
m := &protojson.MarshalOptions{
UseProtoNames: true,
}
if asset.Resource != nil {
CleanStructValue(asset.Resource.Data)
}
glog.V(logRequestsVerboseLevel).Infof("converting asset to golang interface: %v", asset)
buf, err := m.Marshal(asset)
if err != nil {
return nil, errors.Wrapf(err, "marshalling to json with asset %s: %v", asset.Name, asset)
}
var f interface{}
if err := json.Unmarshal(buf, &f); err != nil {
return nil, errors.Wrapf(err, "marshalling from json with asset %s: %v", asset.Name, asset)
}
return f, nil
}
// SanitizeAncestryPath will populate the AncestryPath field from the ancestors list, or fix the pre-populated one
// if no ancestry list is provided.
func SanitizeAncestryPath(asset *validator.Asset) error {
if len(asset.Ancestors) != 0 {
asset.AncestryPath = AncestryPath(asset.Ancestors)
return nil
}
if asset.AncestryPath != "" {
asset.AncestryPath = configs.NormalizeAncestry(asset.AncestryPath)
return nil
}
return errors.Errorf("no ancestry information for asset %s", asset.String())
}
// AncestryPath returns the ancestry path from a given ancestors list
func AncestryPath(ancestors []string) string {
cnt := len(ancestors)
revAncestors := make([]string, len(ancestors))
for idx := 0; idx < cnt; idx++ {
revAncestors[cnt-idx-1] = ancestors[idx]
}
return strings.Join(revAncestors, "/")
}
// ConvertCAIToK8s will convert a supported CAI Asset to a K8S resource and populate any omitted fields.
func ConvertCAIToK8s(asset map[string]interface{}) (*unstructured.Unstructured, error) {
groupKind, found, err := unstructured.NestedString(asset, "asset_type")
if err != nil {
return nil, errors.Wrapf(err, "failed to access asset_type field")
}
if !found {
return nil, errors.Errorf("asset_type field not found")
}
parts := strings.Split(groupKind, "/")
if len(parts) != 2 {
return nil, errors.Errorf("expected asset_type to be of form \"<group>/<kind>\", got %s", groupKind)
}
group := parts[0]
kind := parts[1]
// CAI pretends that the core resources are part of the "k8s.io" apiGroup. For compatibility with what one would
// see in kubernetes, we set the group to empty string ("").
if group == "k8s.io" {
group = ""
}
version, found, err := unstructured.NestedString(asset, "resource", "version")
if err != nil {
return nil, errors.Wrapf(err, "failed to access resource.version field")
}
if !found {
return nil, errors.Errorf("resource.version field not found")
}
resource, found, err := unstructured.NestedMap(asset, "resource", "data")
if err != nil {
return nil, errors.Wrapf(err, "failed to access resource.data field")
}
if !found {
return nil, errors.Errorf("resource.data field not found")
}
u := &unstructured.Unstructured{Object: resource}
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
})
ancestors, found, err := unstructured.NestedStringSlice(asset, "ancestors")
if err != nil {
return nil, errors.Wrapf(err, "failed to access ancestors field")
}
if !found {
return nil, errors.Errorf("ancestors field not found")
}
annotations := u.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations["validator.forsetisecurity.org/ancestorPath"] = AncestryPath(ancestors)
u.SetAnnotations(annotations)
return u, nil
}
// ConvertToAdmissionRequest converts a CAI asset containing a K8S type to an AdmissionRequest which is the format that
// the Gatekeeper Constraint Framework target expects.
func ConvertToAdmissionRequest(asset map[string]interface{}) (*admissionv1beta1.AdmissionRequest, error) {
resource, err := ConvertCAIToK8s(asset)
if err != nil {
return nil, errors.Wrapf(err, "failed to convert CAI asset to k8s resource")
}
resourceJSON, err := json.Marshal(resource.Object)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal k8s resource (converted from CAI asset) to JSON")
}
gvk := resource.GroupVersionKind()
req := &admissionv1beta1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
},
Object: runtime.RawExtension{
Raw: resourceJSON,
},
Name: resource.GetName(),
}
return req, nil
}
// k8s assset names will follow pattern:
// //container.googleapis.com/projects/*/(locations|zones)/*/clusters/*/k8s
var assetPath = regexp.MustCompile(`^//container\.googleapis\.com/projects/[^/]*/(locations|zones)/[^/]*/clusters/[^/]*/k8s`)
// IsK8S returns true if the CAI asset is an asset from a kubernetes cluster.
func IsK8S(asset map[string]interface{}) bool {
assetName, found, err := unstructured.NestedString(asset, "name")
if !found || err != nil {
return false
}
return assetPath.MatchString(assetName)
}