...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient/dcliamclient.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient

     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 iamclient
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/conversion"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/kcclite"
    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/k8s"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    31  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    32  
    33  	dcliam "github.com/GoogleCloudPlatform/declarative-resource-client-library/services/google/iam"
    34  	dclunstruct "github.com/GoogleCloudPlatform/declarative-resource-client-library/unstructured"
    35  	dclunstructiam "github.com/GoogleCloudPlatform/declarative-resource-client-library/unstructured/google/iam"
    36  	"github.com/nasa9084/go-openapi"
    37  	"k8s.io/apimachinery/pkg/api/errors"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/types"
    40  	"sigs.k8s.io/controller-runtime/pkg/client"
    41  )
    42  
    43  type DCLIAMClient struct {
    44  	dclClient  *dcliam.Client
    45  	kubeClient client.Client
    46  	converter  *conversion.Converter
    47  	smLoader   *servicemappingloader.ServiceMappingLoader
    48  }
    49  
    50  func (d *DCLIAMClient) SetPolicyMember(ctx context.Context, tfIAMClient *TFIAMClient, policyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) {
    51  	dclResource, nn, err := d.getDCLResourceAndNamespacedNameFromPolicyMember(ctx, policyMember)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	dclPolicyMember, err := newDCLPolicyMemberFromKRMPolicyMember(ctx, policyMember, dclResource, nn.Namespace, tfIAMClient)
    57  	if err != nil {
    58  		return nil, fmt.Errorf("error converting DCL Policy Member from KRM Policy Member: %w", err)
    59  	}
    60  	dclPolicyMemberResource := dclunstructiam.MemberToUnstructured(dclPolicyMember)
    61  	// DCL's SetPolicyMember returns a Policy not a Member, which is not what this function should return
    62  	_, err = dclunstruct.SetPolicyMember(ctx, d.dclClient.Config, dclResource, dclPolicyMemberResource)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("error setting IAMPolicyMember for resource %v: %w", nn, err)
    65  	}
    66  	return policyMember, nil
    67  }
    68  
    69  func (d *DCLIAMClient) GetPolicyMember(ctx context.Context, tfIAMClient *TFIAMClient, policyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) {
    70  	dclResource, nn, err := d.getDCLResourceAndNamespacedNameFromPolicyMember(ctx, policyMember)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	id, err := ResolveMemberIdentity(ctx, policyMember.Spec.Member, policyMember.Spec.MemberFrom, nn.Namespace, tfIAMClient)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	dclPolicyResource, err := dclunstruct.GetPolicyMember(ctx, d.dclClient.Config,
    79  		dclResource, policyMember.Spec.Role, id)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("error getting IAMPolicyMember for resource %v: %w", nn, err)
    82  	}
    83  	return newIAMPolicyMemberFromDCLResource(dclPolicyResource, policyMember)
    84  }
    85  
    86  func (d *DCLIAMClient) DeletePolicyMember(ctx context.Context, tfIAMClient *TFIAMClient, policyMember *v1beta1.IAMPolicyMember) error {
    87  	dclResource, nn, err := d.getDCLResourceAndNamespacedNameFromPolicyMember(ctx, policyMember)
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	dclPolicyMember, err := newDCLPolicyMemberFromKRMPolicyMember(ctx, policyMember, dclResource, nn.Namespace, tfIAMClient)
    93  	if err != nil {
    94  		return fmt.Errorf("error converting DCL Policy Member from KRM Policy Member: %w", err)
    95  	}
    96  	dclPolicyMemberResource := dclunstructiam.MemberToUnstructured(dclPolicyMember)
    97  
    98  	err = dclunstruct.DeletePolicyMember(ctx, d.dclClient.Config, dclResource, dclPolicyMemberResource)
    99  	if err != nil {
   100  		return fmt.Errorf("error deleting IAMPolicyMember for resource: %w", err)
   101  	}
   102  	return nil
   103  }
   104  
   105  func (d *DCLIAMClient) SetPolicy(ctx context.Context, policy *v1beta1.IAMPolicy) (*v1beta1.IAMPolicy, error) {
   106  	namespace, resourceRef := extractNamespaceAndResourceReference(policy)
   107  	nn := types.NamespacedName{
   108  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   109  		Name:      resourceRef.Name,
   110  	}
   111  	dclSchema, err := d.getSchemaFromResourceReference(resourceRef)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	// Check if the resource supports IAM on DCL.
   117  	supportsIAM, err := extension.HasIam(dclSchema)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	if !supportsIAM {
   122  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", resourceRef.Kind)
   123  	}
   124  
   125  	dclResource, err := d.getDCLResource(ctx, resourceRef, dclSchema, namespace)
   126  	if err != nil {
   127  		return nil, fmt.Errorf("error getting referenced DCL resource, with reference %v: %w", nn, err)
   128  	}
   129  
   130  	dclPolicy, err := newDCLPolicyFromKRMPolicy(policy, dclResource)
   131  	if err != nil {
   132  		return nil, fmt.Errorf("error converting DCLPolicy from KRM Policy: %w", err)
   133  	}
   134  	dclPolicyResource := dclunstructiam.PolicyToUnstructured(dclPolicy)
   135  	liveDCLResource, err := dclunstruct.GetPolicy(ctx, d.dclClient.Config, dclResource)
   136  	if err != nil {
   137  		return nil, fmt.Errorf("error getting IAMPolicy for resource %v: %w", nn, err)
   138  	}
   139  	// Check if resource's live state matches the desired state.
   140  	if reflect.DeepEqual(dclPolicyResource, liveDCLResource) {
   141  		logger.Info("underlying resource is already up to date", "resource", k8s.GetNamespacedName(policy))
   142  		return newIAMPolicyFromDCLResource(liveDCLResource, policy)
   143  	}
   144  	dclPolicyResource, err = dclunstruct.SetPolicyWithEtag(ctx, d.dclClient.Config, dclResource, dclPolicyResource)
   145  	if err != nil {
   146  		return nil, fmt.Errorf("error setting IAMPolicy for resource %v: %w", nn, err)
   147  	}
   148  
   149  	return newIAMPolicyFromDCLResource(dclPolicyResource, policy)
   150  }
   151  
   152  func (d *DCLIAMClient) GetPolicy(ctx context.Context, policy *v1beta1.IAMPolicy) (*v1beta1.IAMPolicy, error) {
   153  	namespace, resourceRef := extractNamespaceAndResourceReference(policy)
   154  	nn := types.NamespacedName{
   155  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   156  		Name:      resourceRef.Name,
   157  	}
   158  	dclSchema, err := d.getSchemaFromResourceReference(resourceRef)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// Check if the resource supports IAM on DCL.
   164  	supportsIAM, err := extension.HasIam(dclSchema)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	if !supportsIAM {
   169  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", resourceRef.Kind)
   170  	}
   171  
   172  	dclResource, err := d.getDCLResource(ctx, resourceRef, dclSchema, namespace)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("error getting referenced DCL resource, with reference %v: %w", nn, err)
   175  	}
   176  
   177  	dclPolicyResource, err := dclunstruct.GetPolicy(ctx, d.dclClient.Config, dclResource)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("error getting IAMPolicy for resource %v: %w", nn, err)
   180  	}
   181  	return newIAMPolicyFromDCLResource(dclPolicyResource, policy)
   182  }
   183  
   184  func (d *DCLIAMClient) DeletePolicy(ctx context.Context, policy *v1beta1.IAMPolicy) error {
   185  	namespace, resourceRef := extractNamespaceAndResourceReference(policy)
   186  	nn := types.NamespacedName{
   187  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   188  		Name:      resourceRef.Name,
   189  	}
   190  	dclSchema, err := d.getSchemaFromResourceReference(resourceRef)
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	// Check if the resource supports IAM on DCL.
   196  	supportsIAM, err := extension.HasIam(dclSchema)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	if !supportsIAM {
   201  		return fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", resourceRef.Kind)
   202  	}
   203  
   204  	dclResource, err := d.getDCLResource(ctx, resourceRef, dclSchema, namespace)
   205  	if err != nil {
   206  		return fmt.Errorf("error getting referenced DCL resource, with reference %v: %w", nn, err)
   207  	}
   208  
   209  	dclPolicyResource, err := dclunstruct.GetPolicy(ctx, d.dclClient.Config, dclResource)
   210  	if err != nil {
   211  		return fmt.Errorf("error getting IAMPolicy for resource%v: %w", nn, err)
   212  	}
   213  
   214  	// Setting an empty policy is technically deleting the policy as DCL only
   215  	// mirrors the get/set calls from the API for now.
   216  	emptyPolicy := setEmptyPolicyForDeletion(dclPolicyResource)
   217  	_, err = dclunstruct.SetPolicy(ctx, d.dclClient.Config, dclResource, emptyPolicy)
   218  	if err != nil {
   219  		return fmt.Errorf("error deleting IAMPolicy for resource: %w", err)
   220  	}
   221  	return nil
   222  }
   223  
   224  func (d *DCLIAMClient) getSchemaFromResourceReference(resourceRef v1beta1.ResourceReference) (*openapi.Schema, error) {
   225  	gvk := resourceRef.GroupVersionKind()
   226  	// ExternalOnlyGVKs are only supported by TF.
   227  	if externalonlygvks.IsExternalOnlyGVK(gvk) {
   228  		return nil, fmt.Errorf("invalid DCL resource reference type: kind %v is not supported", resourceRef.Kind)
   229  	}
   230  
   231  	dclSchema, err := dclschemaloader.GetDCLSchemaForGVK(gvk,
   232  		d.converter.MetadataLoader, d.converter.SchemaLoader)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	return dclSchema, nil
   237  }
   238  
   239  func (d *DCLIAMClient) getDCLResourceAndNamespacedNameFromPolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) (*dclunstruct.Resource, types.NamespacedName, error) {
   240  	namespace, resourceRef := extractNamespaceAndResourceReference(policyMember)
   241  	nn := types.NamespacedName{
   242  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   243  		Name:      resourceRef.Name,
   244  	}
   245  	dclSchema, err := d.getSchemaFromResourceReference(resourceRef)
   246  	if err != nil {
   247  		return nil, nn, err
   248  	}
   249  
   250  	// Check if the resource supports IAM on DCL.
   251  	supportsIAM, err := extension.HasIam(dclSchema)
   252  	if err != nil {
   253  		return nil, nn, err
   254  	}
   255  	if !supportsIAM {
   256  		return nil, nn, fmt.Errorf("invalid resource reference: kind %v does not support IAM policy member", resourceRef.Kind)
   257  	}
   258  
   259  	dclResource, err := d.getDCLResource(ctx, resourceRef, dclSchema, namespace)
   260  	if err != nil {
   261  		return nil, nn, fmt.Errorf("error getting referenced DCL resource, with reference %v: %w", nn, err)
   262  	}
   263  	return dclResource, nn, nil
   264  }
   265  
   266  func (d *DCLIAMClient) getDCLResource(ctx context.Context, resourceRef v1beta1.ResourceReference,
   267  	dclSchema *openapi.Schema, namespace string) (*dclunstruct.Resource, error) {
   268  	gvk := resourceRef.GroupVersionKind()
   269  	u := &unstructured.Unstructured{}
   270  	u.SetGroupVersionKind(gvk)
   271  	nn := types.NamespacedName{
   272  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   273  		Name:      resourceRef.Name,
   274  	}
   275  	if err := d.kubeClient.Get(ctx, nn, u); err != nil {
   276  		if errors.IsNotFound(err) {
   277  			return nil, k8s.NewReferenceNotFoundError(gvk, nn)
   278  		}
   279  		return nil, fmt.Errorf("error retrieving resource '%v' with GroupVersionKind '%v': %w", nn, gvk, err)
   280  	}
   281  	resource, err := dcl.NewResource(u, dclSchema)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	lite, err := kcclite.ToKCCLiteBestEffort(resource, d.converter.MetadataLoader, d.converter.SchemaLoader, d.smLoader, d.kubeClient)
   286  	if err != nil {
   287  		return nil, fmt.Errorf("error converting KCC full to KCC lite: %w", err)
   288  	}
   289  	dclResource, err := d.converter.KRMObjectToDCLObject(lite)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	return dclResource, nil
   294  }
   295  
   296  func setEmptyPolicyForDeletion(dclPolicyResource *dclunstruct.Resource) *dclunstruct.Resource {
   297  	dclPolicyResource.Object["bindings"] = []interface{}{}
   298  	dclPolicyResource.Object["etag"] = ""
   299  	return dclPolicyResource
   300  }
   301  
   302  // This function is only used in SetPolicy to convert the newly requested KRMPolicy to a
   303  // DCL policy resource that can interact with DCL's IAMClient.
   304  func newDCLPolicyFromKRMPolicy(policy *v1beta1.IAMPolicy, dclResource *dclunstruct.Resource) (*dcliam.Policy, error) {
   305  	if policy.Spec.AuditConfigs != nil {
   306  		return nil, fmt.Errorf("policy resource contains AuditConfigs which are not currently supported by DCL-based resources")
   307  	}
   308  	return &dcliam.Policy{
   309  		Bindings: kccToDCLBindings(policy.Spec.Bindings),
   310  		Etag:     &policy.Spec.Etag,
   311  		Resource: dclResource,
   312  	}, nil
   313  }
   314  
   315  // Converts a DCL IAMPolicy resource into a KRM IAMPolicy resource.
   316  func newIAMPolicyFromDCLResource(dclResource *dclunstruct.Resource, origPolicy *v1beta1.IAMPolicy) (*v1beta1.IAMPolicy, error) {
   317  	if dclResource.STV.Service != "iam" || dclResource.STV.Type != "Policy" {
   318  		return nil, fmt.Errorf("dclResource was not IAMPolicy instead was service %s and type %s", dclResource.STV.Service, dclResource.STV.Type)
   319  	}
   320  	iamPolicy := v1beta1.IAMPolicy{}
   321  	iamPolicy.ObjectMeta = origPolicy.ObjectMeta
   322  	iamPolicy.Spec.ResourceReference = origPolicy.Spec.ResourceReference
   323  	if dclResource.Object["bindings"] != nil {
   324  		binds, err := dclToKCCBindings(dclResource.Object["bindings"].([]interface{}))
   325  		if err != nil {
   326  			return nil, fmt.Errorf("error converting DCL Bindings to KCC Bindings: %w", err)
   327  		}
   328  		iamPolicy.Spec.Bindings = binds
   329  	}
   330  	etag := dclResource.Object["etag"].(string)
   331  	if etag == "" {
   332  		return nil, fmt.Errorf("unexpected empty etag read from the IAMPolicy resource")
   333  	}
   334  	iamPolicy.Spec.Etag = etag
   335  	return &iamPolicy, nil
   336  }
   337  
   338  // Converts a DCL IAMPolicyMember resource into a KRM IAMPolicyMember resource.
   339  func newIAMPolicyMemberFromDCLResource(dclMemberResource *dclunstruct.Resource, origPolicyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) {
   340  	if dclMemberResource.STV.Service != "iam" || dclMemberResource.STV.Type != "PolicyMember" {
   341  		return nil, fmt.Errorf("dclResource was not IAM Member instead was service %s and type %s", dclMemberResource.STV.Service, dclMemberResource.STV.Type)
   342  	}
   343  	iamPolicyMember := v1beta1.IAMPolicyMember{}
   344  	iamPolicyMember.ObjectMeta = origPolicyMember.ObjectMeta
   345  	iamPolicyMember.Spec = v1beta1.IAMPolicyMemberSpec{
   346  		ResourceReference: origPolicyMember.Spec.ResourceReference,
   347  		Role:              dclMemberResource.Object["role"].(string),
   348  	}
   349  	if dclMemberResource.Object["member"] != nil {
   350  		member := dclMemberResource.Object["member"].(string)
   351  		iamPolicyMember.Spec.Member = v1beta1.Member(member)
   352  	}
   353  	if origPolicyMember.Spec.Member == "" {
   354  		iamPolicyMember.Spec.Member = ""
   355  		iamPolicyMember.Spec.MemberFrom = origPolicyMember.Spec.MemberFrom
   356  	}
   357  	return &iamPolicyMember, nil
   358  }
   359  
   360  // This function is only used in SetPolicyMember to convert the newly requested KRMPolicyMember to a
   361  // DCL policy resource that can interact with DCL's IAMClient.
   362  func newDCLPolicyMemberFromKRMPolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember,
   363  	dclResource *dclunstruct.Resource, namespace string,
   364  	tfIAMClient *TFIAMClient) (*dcliam.Member, error) {
   365  	member, err := ResolveMemberIdentity(ctx, policyMember.Spec.Member, policyMember.Spec.MemberFrom, namespace, tfIAMClient)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	// TODO(b/228226694): DCL-based resources don't currently support Member conditions.
   370  	if policyMember.Spec.Condition != nil {
   371  		return nil, fmt.Errorf("the IAMPolicyMember for this resource of gvk %v does not currently support conditions", policyMember.Spec.ResourceReference.GroupVersionKind())
   372  	}
   373  	return &dcliam.Member{
   374  		Role:     &policyMember.Spec.Role,
   375  		Member:   &member,
   376  		Resource: dclResource,
   377  	}, nil
   378  }
   379  
   380  func kccToDCLBindings(kccBindings []v1beta1.IAMPolicyBinding) []dcliam.Binding {
   381  	ret := make([]dcliam.Binding, len(kccBindings))
   382  	for i, bind := range kccBindings {
   383  		role := bind.Role
   384  		ret[i] = dcliam.Binding{
   385  			Role:      &role,
   386  			Members:   convertMembersToStringArr(bind.Members),
   387  			Condition: kccToDCLCondition(bind.Condition),
   388  		}
   389  	}
   390  	return ret
   391  }
   392  
   393  func kccToDCLCondition(condition *v1beta1.IAMCondition) *dcliam.Condition {
   394  	if condition == nil {
   395  		return nil
   396  	}
   397  	return &dcliam.Condition{
   398  		Title:       &condition.Title,
   399  		Description: &condition.Description,
   400  		Expression:  &condition.Expression,
   401  	}
   402  }
   403  
   404  func dclToKCCBindings(dclBindings []interface{}) ([]v1beta1.IAMPolicyBinding, error) {
   405  	if dclBindings == nil {
   406  		return nil, nil
   407  	}
   408  	ret := make([]v1beta1.IAMPolicyBinding, len(dclBindings))
   409  	for i, bind := range dclBindings {
   410  		binding := bind.(map[string]interface{})
   411  		var cond *v1beta1.IAMCondition
   412  		if binding["condition"] != nil {
   413  			cond = &v1beta1.IAMCondition{}
   414  			if err := util.Marshal(binding["condition"], cond); err != nil {
   415  				return nil, err
   416  			}
   417  		}
   418  		ret[i] = v1beta1.IAMPolicyBinding{
   419  			Members:   convertMapToMemberArr(binding),
   420  			Role:      binding["role"].(string), // no nil check needed because required value
   421  			Condition: cond,
   422  		}
   423  	}
   424  	return ret, nil
   425  }
   426  
   427  func convertMapToMemberArr(binding map[string]interface{}) []v1beta1.Member {
   428  	if binding["members"] == nil {
   429  		return nil
   430  	}
   431  	members := binding["members"].([]interface{})
   432  	var ret []v1beta1.Member
   433  	for _, val := range members {
   434  		memberStr := val.(string)
   435  		ret = append(ret, v1beta1.Member(memberStr))
   436  	}
   437  	return ret
   438  }
   439  
   440  func convertMembersToStringArr(members []v1beta1.Member) []string {
   441  	var ret []string
   442  	for _, val := range members {
   443  		ret = append(ret, string(val))
   444  	}
   445  	return ret
   446  }
   447  

View as plain text