...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/iam_validator.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package webhook
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/http"
    21  
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1"
    24  	kcciamclient "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gvks/externalonlygvks"
    29  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    31  
    32  	"github.com/nasa9084/go-openapi"
    33  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  	"k8s.io/klog/v2"
    36  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    37  )
    38  
    39  type iamValidatorHandler struct {
    40  	smLoader              *servicemappingloader.ServiceMappingLoader
    41  	serviceMetadataLoader metadata.ServiceMetadataLoader
    42  	schemaLoader          dclschemaloader.DCLSchemaLoader
    43  }
    44  
    45  func NewIAMValidatorHandler(smLoader *servicemappingloader.ServiceMappingLoader,
    46  	serviceMetadataLoader metadata.ServiceMetadataLoader,
    47  	schemaLoader dclschemaloader.DCLSchemaLoader) *iamValidatorHandler {
    48  	return &iamValidatorHandler{
    49  		smLoader:              smLoader,
    50  		serviceMetadataLoader: serviceMetadataLoader,
    51  		schemaLoader:          schemaLoader,
    52  	}
    53  }
    54  
    55  func (a *iamValidatorHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
    56  	deserializer := codecs.UniversalDeserializer()
    57  	obj := &unstructured.Unstructured{}
    58  	if _, _, err := deserializer.Decode(req.AdmissionRequest.Object.Raw, nil, obj); err != nil {
    59  		klog.Error(err)
    60  		return admission.Errored(http.StatusBadRequest,
    61  			fmt.Errorf("error decoding object: %v", err))
    62  	}
    63  	switch {
    64  	case isIAMPolicy(obj):
    65  		policy, err := toIAMPolicy(obj)
    66  		if err != nil {
    67  			return admission.Errored(http.StatusInternalServerError, err)
    68  		}
    69  		refResourceGVK := policy.Spec.ResourceReference.GroupVersionKind()
    70  		isDCLResource := metadata.IsDCLBasedResourceKind(refResourceGVK, a.serviceMetadataLoader)
    71  		return a.validateIAMPolicy(policy, isDCLResource)
    72  
    73  	case isIAMPartialPolicy(obj):
    74  		partialPolicy, err := toIAMPartialPolicy(obj)
    75  		if err != nil {
    76  			return admission.Errored(http.StatusInternalServerError, err)
    77  		}
    78  		refResourceGVK := partialPolicy.Spec.ResourceReference.GroupVersionKind()
    79  		isDCLResource := metadata.IsDCLBasedResourceKind(refResourceGVK, a.serviceMetadataLoader)
    80  		return a.validateIAMPartialPolicy(partialPolicy, isDCLResource)
    81  
    82  	case isIAMPolicyMember(obj):
    83  		policyMember, err := toIAMPolicyMember(obj)
    84  		if err != nil {
    85  			return admission.Errored(http.StatusInternalServerError, err)
    86  		}
    87  		refResourceGVK := policyMember.Spec.ResourceReference.GroupVersionKind()
    88  		isDCLResource := metadata.IsDCLBasedResourceKind(refResourceGVK, a.serviceMetadataLoader)
    89  		return a.validateIAMPolicyMember(policyMember, isDCLResource)
    90  	case isIAMAuditConfig(obj):
    91  		auditConfig, err := toIAMAuditConfig(obj)
    92  		if err != nil {
    93  			return admission.Errored(http.StatusInternalServerError, err)
    94  		}
    95  		refResourceGVK := auditConfig.Spec.ResourceReference.GroupVersionKind()
    96  		isDCLResource := metadata.IsDCLBasedResourceKind(refResourceGVK, a.serviceMetadataLoader)
    97  		if isDCLResource {
    98  			return admission.Errored(http.StatusForbidden,
    99  				fmt.Errorf("object of GroupVersionKind %v does not have IAM Audit Config support", obj.GroupVersionKind()))
   100  		}
   101  		rcs, err := getResourceConfigs(a.smLoader, refResourceGVK)
   102  		if err != nil {
   103  			return admission.Errored(http.StatusBadRequest, err)
   104  		}
   105  		return validateIAMAuditConfig(auditConfig, rcs)
   106  	default:
   107  		return admission.Errored(http.StatusInternalServerError,
   108  			fmt.Errorf("object of GroupVersionKind %v is not a supported IAM resource", obj.GroupVersionKind()))
   109  	}
   110  }
   111  
   112  func toIAMPolicy(obj *unstructured.Unstructured) (*v1beta1.IAMPolicy, error) {
   113  	policy := &v1beta1.IAMPolicy{}
   114  	if err := util.Marshal(obj, policy); err != nil {
   115  		return nil, fmt.Errorf("error parsing %v into IAM Policy object: %v", obj.GetName(), err)
   116  	}
   117  	return policy, nil
   118  }
   119  
   120  func toIAMPartialPolicy(obj *unstructured.Unstructured) (*v1beta1.IAMPartialPolicy, error) {
   121  	partialPolicy := &v1beta1.IAMPartialPolicy{}
   122  	if err := util.Marshal(obj, partialPolicy); err != nil {
   123  		return nil, fmt.Errorf("error parsing %v into IAMPartialPolicy object: %v", obj.GetName(), err)
   124  	}
   125  	return partialPolicy, nil
   126  }
   127  
   128  func toIAMPolicyMember(obj *unstructured.Unstructured) (*v1beta1.IAMPolicyMember, error) {
   129  	policyMember := &v1beta1.IAMPolicyMember{}
   130  	if err := util.Marshal(obj, policyMember); err != nil {
   131  		return nil, fmt.Errorf("error parsing %v into IAM Policy Member object: %v", obj.GetName(), err)
   132  	}
   133  	return policyMember, nil
   134  }
   135  
   136  func toIAMAuditConfig(obj *unstructured.Unstructured) (*v1beta1.IAMAuditConfig, error) {
   137  	auditConfig := &v1beta1.IAMAuditConfig{}
   138  	if err := util.Marshal(obj, auditConfig); err != nil {
   139  		return nil, fmt.Errorf("error parsing %v into IAMAuditConfig object: %v", obj.GetName(), err)
   140  	}
   141  	return auditConfig, nil
   142  }
   143  
   144  func getDCLSchema(gvk schema.GroupVersionKind, serviceMetadataLoader metadata.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader) (*openapi.Schema, admission.Response) {
   145  	dclSchema, err := dclschemaloader.GetDCLSchemaForGVK(gvk, serviceMetadataLoader, schemaLoader)
   146  	if err != nil {
   147  		return nil, admission.Errored(http.StatusBadRequest, err)
   148  	}
   149  	return dclSchema, allowedResponse
   150  }
   151  
   152  func getResourceConfigs(smLoader *servicemappingloader.ServiceMappingLoader, gvk schema.GroupVersionKind) ([]*v1alpha1.ResourceConfig, error) {
   153  	// Support the case where the user specified the kind as "Project" and nothing else.
   154  	// TODO(kcc-eng): Remove once we drop headless IAM support.
   155  	if gvk.Group == "" {
   156  		if gvk.Kind == kcciamclient.ProjectKind {
   157  			gvk = kcciamclient.ProjectGVK
   158  		} else {
   159  			return []*v1alpha1.ResourceConfig{}, fmt.Errorf("resource reference for kind '%v' must include API group", gvk.Kind)
   160  		}
   161  	}
   162  	if externalonlygvks.IsExternalOnlyGVK(gvk) {
   163  		rc, err := kcciamclient.GetResourceConfigForExternalOnlyGVK(gvk)
   164  		if err != nil {
   165  			return []*v1alpha1.ResourceConfig{}, fmt.Errorf("error getting ResourceConfig for GroupVersionKind %v: %v", gvk, err)
   166  		}
   167  		return []*v1alpha1.ResourceConfig{rc}, nil
   168  	}
   169  	rcs, err := smLoader.GetResourceConfigs(gvk)
   170  	if err != nil {
   171  		return []*v1alpha1.ResourceConfig{}, fmt.Errorf("error getting ResourceConfig for GroupVersionKind %v: %v", gvk, err)
   172  	}
   173  	if len(rcs) == 0 {
   174  		return []*v1alpha1.ResourceConfig{}, fmt.Errorf("couldn't find any ResourceConfig defined for GroupVersionKind %v", gvk)
   175  	}
   176  	return rcs, nil
   177  }
   178  
   179  func (a *iamValidatorHandler) validateIAMPolicy(policy *v1beta1.IAMPolicy, isDCLResource bool) admission.Response {
   180  	resourceRef := policy.Spec.ResourceReference
   181  	if isDCLResource {
   182  		return a.dclValidateIAMPolicy(policy)
   183  	}
   184  
   185  	// TF-based resource.
   186  	rcs, err := getResourceConfigs(a.smLoader, resourceRef.GroupVersionKind())
   187  	if err != nil {
   188  		return admission.Errored(http.StatusBadRequest, err)
   189  	}
   190  	return a.tfValidateIAMPolicy(policy, rcs)
   191  }
   192  
   193  func (a *iamValidatorHandler) validateIAMPartialPolicy(partialPolicy *v1beta1.IAMPartialPolicy, isDCLResource bool) admission.Response {
   194  	resourceRef := partialPolicy.Spec.ResourceReference
   195  	if isDCLResource {
   196  		return a.dclValidateIAMPartialPolicy(partialPolicy)
   197  	}
   198  	// TF-based resource.
   199  	rcs, err := getResourceConfigs(a.smLoader, resourceRef.GroupVersionKind())
   200  	if err != nil {
   201  		return admission.Errored(http.StatusBadRequest, err)
   202  	}
   203  	return a.tfValidateIAMPartialPolicy(partialPolicy, rcs)
   204  }
   205  
   206  func (a *iamValidatorHandler) validateIAMPolicyMember(policyMember *v1beta1.IAMPolicyMember, isDCLResource bool) admission.Response {
   207  	resourceRef := policyMember.Spec.ResourceReference
   208  	if isDCLResource {
   209  		return a.dclValidateIAMPolicyMember(policyMember)
   210  	}
   211  	// TF-based resource.
   212  	rcs, err := getResourceConfigs(a.smLoader, resourceRef.GroupVersionKind())
   213  	if err != nil {
   214  		return admission.Errored(http.StatusBadRequest, err)
   215  	}
   216  	return a.tfValidateIAMPolicyMember(policyMember, rcs)
   217  }
   218  
   219  func validateIAMAuditConfig(auditConfig *v1beta1.IAMAuditConfig, refResourceRCs []*v1alpha1.ResourceConfig) admission.Response {
   220  	resourceRef := auditConfig.Spec.ResourceReference
   221  	if !doesTFResourceSupportAuditConfigs(refResourceRCs) {
   222  		return admission.Errored(http.StatusForbidden,
   223  			fmt.Errorf("GroupVersionKind %v does not support IAM Audit Configs", resourceRef.GroupVersionKind()))
   224  	}
   225  	return allowedResponse
   226  }
   227  
   228  func (a *iamValidatorHandler) dclValidateIAMPolicy(policy *v1beta1.IAMPolicy) admission.Response {
   229  	resourceRef := policy.Spec.ResourceReference
   230  	// Check that DCL-based resource supports IAMPolicy
   231  	dclSchema, resp := getDCLSchema(resourceRef.GroupVersionKind(), a.serviceMetadataLoader, a.schemaLoader)
   232  	if !resp.Allowed {
   233  		return resp
   234  	}
   235  	supportsIAM, err := extension.HasIam(dclSchema)
   236  	if err != nil {
   237  		return admission.Errored(http.StatusInternalServerError, err)
   238  	}
   239  	if !supportsIAM {
   240  		return admission.Errored(http.StatusForbidden, fmt.Errorf("GroupVersionKind %v does not support IAM Policy", resourceRef.GroupVersionKind()))
   241  	}
   242  	// Currently, DCL-based resources that have IAMPolicy also support IAMConditions
   243  	// and we don't need to check for conditions.
   244  	// TODO: (b/182505291) DCL-based resources do not currently support IAMAuditConfigs
   245  	if len(policy.Spec.AuditConfigs) > 0 {
   246  		return admission.Errored(http.StatusForbidden, fmt.Errorf("GroupVersionKind %v does not support IAM Audit Configs", resourceRef.GroupVersionKind()))
   247  	}
   248  	return allowedResponse
   249  }
   250  
   251  func (a *iamValidatorHandler) tfValidateIAMPolicy(policy *v1beta1.IAMPolicy, rcs []*v1alpha1.ResourceConfig) admission.Response {
   252  	resourceRef := policy.Spec.ResourceReference
   253  	if doesIAMPolicyHaveConditions(policy) && !doesTFResourceSupportConditions(rcs) {
   254  		return admission.Errored(http.StatusForbidden,
   255  			fmt.Errorf("GroupVersionKind %v does not support IAM Conditions", resourceRef.GroupVersionKind()))
   256  	}
   257  	if len(policy.Spec.AuditConfigs) > 0 && !doesTFResourceSupportAuditConfigs(rcs) {
   258  		return admission.Errored(http.StatusForbidden,
   259  			fmt.Errorf("GroupVersionKind %v does not support IAM Audit Configs", resourceRef.GroupVersionKind()))
   260  	}
   261  	return allowedResponse
   262  }
   263  
   264  func (a *iamValidatorHandler) dclValidateIAMPartialPolicy(partialPolicy *v1beta1.IAMPartialPolicy) admission.Response {
   265  	resourceRef := partialPolicy.Spec.ResourceReference
   266  	// Check that DCL-based resource supports IAMPolicy
   267  	dclSchema, resp := getDCLSchema(resourceRef.GroupVersionKind(), a.serviceMetadataLoader, a.schemaLoader)
   268  	if !resp.Allowed {
   269  		return resp
   270  	}
   271  	supportsIAM, err := extension.HasIam(dclSchema)
   272  	if err != nil {
   273  		return admission.Errored(http.StatusInternalServerError, err)
   274  	}
   275  	if !supportsIAM {
   276  		return admission.Errored(http.StatusForbidden, fmt.Errorf("GroupVersionKind %v does not support IAM Partial Policy", resourceRef.GroupVersionKind()))
   277  	}
   278  	return allowedResponse
   279  }
   280  
   281  func (a *iamValidatorHandler) tfValidateIAMPartialPolicy(partialPolicy *v1beta1.IAMPartialPolicy, rcs []*v1alpha1.ResourceConfig) admission.Response {
   282  	resourceRef := partialPolicy.Spec.ResourceReference
   283  	if doesIAMPartialPolicyHaveConditions(partialPolicy) && !doesTFResourceSupportConditions(rcs) {
   284  		return admission.Errored(http.StatusForbidden,
   285  			fmt.Errorf("GroupVersionKind %v does not support IAM Conditions", resourceRef.GroupVersionKind()))
   286  	}
   287  	return allowedResponse
   288  }
   289  
   290  func (a *iamValidatorHandler) dclValidateIAMPolicyMember(policyMember *v1beta1.IAMPolicyMember) admission.Response {
   291  	resourceRef := policyMember.Spec.ResourceReference
   292  	// Check that DCL-based resource supports IAMPolicy
   293  	dclSchema, resp := getDCLSchema(resourceRef.GroupVersionKind(), a.serviceMetadataLoader, a.schemaLoader)
   294  	if !resp.Allowed {
   295  		return resp
   296  	}
   297  	supportsIAM, err := extension.HasIam(dclSchema)
   298  	if err != nil {
   299  		return admission.Errored(http.StatusInternalServerError, err)
   300  	}
   301  	if !supportsIAM {
   302  		return admission.Errored(http.StatusForbidden, fmt.Errorf("GroupVersionKind %v does not support IAM Policy Member", resourceRef.GroupVersionKind()))
   303  	}
   304  	// TODO (b/228226694): IAMPolicyMember does not currently support conditions.
   305  	if doesIAMPolicyMemberHaveCondition(policyMember) {
   306  		return admission.Errored(http.StatusForbidden,
   307  			fmt.Errorf("GroupVersionKind %v does not support IAM Conditions in IAM Policy Member", resourceRef.GroupVersionKind()))
   308  	}
   309  	return allowedResponse
   310  }
   311  
   312  func (a *iamValidatorHandler) tfValidateIAMPolicyMember(policyMember *v1beta1.IAMPolicyMember, rcs []*v1alpha1.ResourceConfig) admission.Response {
   313  	resourceRef := policyMember.Spec.ResourceReference
   314  	if doesIAMPolicyMemberHaveCondition(policyMember) && !doesTFResourceSupportConditions(rcs) {
   315  		return admission.Errored(http.StatusForbidden,
   316  			fmt.Errorf("GroupVersionKind %v does not support IAM Conditions", resourceRef.GroupVersionKind()))
   317  	}
   318  	return allowedResponse
   319  }
   320  
   321  func doesIAMPolicyHaveConditions(policy *v1beta1.IAMPolicy) bool {
   322  	for _, binding := range policy.Spec.Bindings {
   323  		if binding.Condition != nil {
   324  			return true
   325  		}
   326  	}
   327  	return false
   328  }
   329  
   330  func doesIAMPartialPolicyHaveConditions(partialPolicy *v1beta1.IAMPartialPolicy) bool {
   331  	for _, binding := range partialPolicy.Spec.Bindings {
   332  		if binding.Condition != nil {
   333  			return true
   334  		}
   335  	}
   336  	return false
   337  }
   338  
   339  func doesIAMPolicyMemberHaveCondition(policyMember *v1beta1.IAMPolicyMember) bool {
   340  	return policyMember.Spec.Condition != nil
   341  }
   342  
   343  func doesTFResourceSupportConditions(rcs []*v1alpha1.ResourceConfig) bool {
   344  	// All ResourceConfigs for a given kind have the same value for IAMConfig.SupportsConditions.
   345  	return rcs[0].IAMConfig.SupportsConditions
   346  }
   347  
   348  func doesTFResourceSupportAuditConfigs(rcs []*v1alpha1.ResourceConfig) bool {
   349  	// All ResourceConfigs for a given kind support or don't support IAM audit configs.
   350  	return rcs[0].IAMConfig.AuditConfigName != ""
   351  }
   352  

View as plain text