sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/eks/iam/iam.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package iam
    18  
    19  import (
    20  	"crypto/sha1"
    21  	"encoding/hex"
    22  	"encoding/json"
    23  	"net/http"
    24  	"net/url"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	"github.com/aws/aws-sdk-go/service/eks"
    28  	"github.com/aws/aws-sdk-go/service/iam"
    29  	"github.com/aws/aws-sdk-go/service/iam/iamiface"
    30  	"github.com/go-logr/logr"
    31  	"github.com/google/go-cmp/cmp"
    32  	"github.com/pkg/errors"
    33  
    34  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    35  	"sigs.k8s.io/cluster-api-provider-aws/cmd/clusterawsadm/converters"
    36  	iamv1 "sigs.k8s.io/cluster-api-provider-aws/iam/api/v1beta1"
    37  )
    38  
    39  const (
    40  	// EKSFargateService is the service to trust for fargate pod execution roles.
    41  	EKSFargateService = "eks-fargate-pods.amazonaws.com"
    42  )
    43  
    44  // IAMService defines the specs for an IAM service.
    45  type IAMService struct {
    46  	logr.Logger
    47  	IAMClient iamiface.IAMAPI
    48  }
    49  
    50  // GetIAMRole will return the IAM role for the IAMService.
    51  func (s *IAMService) GetIAMRole(name string) (*iam.Role, error) {
    52  	input := &iam.GetRoleInput{
    53  		RoleName: aws.String(name),
    54  	}
    55  
    56  	out, err := s.IAMClient.GetRole(input)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	return out.Role, nil
    62  }
    63  
    64  func (s *IAMService) getIAMPolicy(policyArn string) (*iam.Policy, error) {
    65  	input := &iam.GetPolicyInput{
    66  		PolicyArn: &policyArn,
    67  	}
    68  
    69  	out, err := s.IAMClient.GetPolicy(input)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	return out.Policy, nil
    75  }
    76  
    77  func (s *IAMService) getIAMRolePolicies(roleName string) ([]*string, error) {
    78  	input := &iam.ListAttachedRolePoliciesInput{
    79  		RoleName: &roleName,
    80  	}
    81  
    82  	out, err := s.IAMClient.ListAttachedRolePolicies(input)
    83  	if err != nil {
    84  		return nil, errors.Wrapf(err, "error listing role polices for %s", roleName)
    85  	}
    86  
    87  	policies := []*string{}
    88  	for _, policy := range out.AttachedPolicies {
    89  		policies = append(policies, policy.PolicyArn)
    90  	}
    91  
    92  	return policies, nil
    93  }
    94  
    95  func (s *IAMService) detachIAMRolePolicy(roleName string, policyARN string) error {
    96  	input := &iam.DetachRolePolicyInput{
    97  		RoleName:  aws.String(roleName),
    98  		PolicyArn: aws.String(policyARN),
    99  	}
   100  
   101  	if _, err := s.IAMClient.DetachRolePolicy(input); err != nil {
   102  		return errors.Wrapf(err, "error detaching policy %s from role %s", policyARN, roleName)
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  func (s *IAMService) attachIAMRolePolicy(roleName string, policyARN string) error {
   109  	input := &iam.AttachRolePolicyInput{
   110  		RoleName:  aws.String(roleName),
   111  		PolicyArn: aws.String(policyARN),
   112  	}
   113  
   114  	if _, err := s.IAMClient.AttachRolePolicy(input); err != nil {
   115  		return errors.Wrapf(err, "error attaching policy %s to role %s", policyARN, roleName)
   116  	}
   117  
   118  	return nil
   119  }
   120  
   121  // EnsurePoliciesAttached will ensure the IAMService has policies attached.
   122  func (s *IAMService) EnsurePoliciesAttached(role *iam.Role, policies []*string) (bool, error) {
   123  	s.V(2).Info("Ensuring Polices are attached to role")
   124  	existingPolices, err := s.getIAMRolePolicies(*role.RoleName)
   125  	if err != nil {
   126  		return false, err
   127  	}
   128  
   129  	var updatedPolicies bool
   130  	// Remove polices that aren't in the list
   131  	for _, existingPolicy := range existingPolices {
   132  		found := findStringInSlice(policies, *existingPolicy)
   133  		if !found {
   134  			updatedPolicies = true
   135  			err = s.detachIAMRolePolicy(*role.RoleName, *existingPolicy)
   136  			if err != nil {
   137  				return false, err
   138  			}
   139  			s.V(2).Info("Detached policy from role", "role", role.RoleName, "policy", existingPolicy)
   140  		}
   141  	}
   142  
   143  	// Add any policies that aren't currently attached
   144  	for _, policy := range policies {
   145  		found := findStringInSlice(existingPolices, *policy)
   146  		if !found {
   147  			// Make sure policy exists before attaching
   148  			_, err := s.getIAMPolicy(*policy)
   149  			if err != nil {
   150  				return false, errors.Wrapf(err, "error getting policy %s", *policy)
   151  			}
   152  
   153  			updatedPolicies = true
   154  			err = s.attachIAMRolePolicy(*role.RoleName, *policy)
   155  			if err != nil {
   156  				return false, err
   157  			}
   158  			s.V(2).Info("Attached policy to role", "role", role.RoleName, "policy", *policy)
   159  		}
   160  	}
   161  
   162  	return updatedPolicies, nil
   163  }
   164  
   165  // RoleTags returns the tags for the given role.
   166  func RoleTags(key string, additionalTags infrav1.Tags) []*iam.Tag {
   167  	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(key)] = string(infrav1.ResourceLifecycleOwned)
   168  	tags := []*iam.Tag{}
   169  	for k, v := range additionalTags {
   170  		tags = append(tags, &iam.Tag{
   171  			Key:   aws.String(k),
   172  			Value: aws.String(v),
   173  		})
   174  	}
   175  	return tags
   176  }
   177  
   178  // CreateRole will create a role from the IAMService.
   179  func (s *IAMService) CreateRole(
   180  	roleName string,
   181  	key string,
   182  	trustRelationship *iamv1.PolicyDocument,
   183  	additionalTags infrav1.Tags,
   184  ) (*iam.Role, error) {
   185  	tags := RoleTags(key, additionalTags)
   186  
   187  	trustRelationshipJSON, err := converters.IAMPolicyDocumentToJSON(*trustRelationship)
   188  	if err != nil {
   189  		return nil, errors.Wrap(err, "error converting trust relationship to json")
   190  	}
   191  
   192  	input := &iam.CreateRoleInput{
   193  		RoleName:                 aws.String(roleName),
   194  		Tags:                     tags,
   195  		AssumeRolePolicyDocument: aws.String(trustRelationshipJSON),
   196  	}
   197  
   198  	out, err := s.IAMClient.CreateRole(input)
   199  	if err != nil {
   200  		return nil, errors.Wrap(err, "failed to call CreateRole")
   201  	}
   202  
   203  	return out.Role, nil
   204  }
   205  
   206  // EnsureTagsAndPolicy will ensure any tags and policies against the IAMService.
   207  func (s *IAMService) EnsureTagsAndPolicy(
   208  	role *iam.Role,
   209  	key string,
   210  	trustRelationship *iamv1.PolicyDocument,
   211  	additionalTags infrav1.Tags,
   212  ) (bool, error) {
   213  	s.V(2).Info("Ensuring tags and AssumeRolePolicyDocument are set on role")
   214  
   215  	rolePolicyDocumentRaw, err := url.PathUnescape(*role.AssumeRolePolicyDocument)
   216  	if err != nil {
   217  		return false, errors.Wrap(err, "couldn't decode AssumeRolePolicyDocument")
   218  	}
   219  
   220  	var rolePolicyDocument iamv1.PolicyDocument
   221  	err = json.Unmarshal([]byte(rolePolicyDocumentRaw), &rolePolicyDocument)
   222  	if err != nil {
   223  		return false, errors.Wrap(err, "couldn't unmarshal AssumeRolePolicyDocument")
   224  	}
   225  
   226  	var updated bool
   227  	if !cmp.Equal(*trustRelationship, rolePolicyDocument) {
   228  		trustRelationshipJSON, err := converters.IAMPolicyDocumentToJSON(*trustRelationship)
   229  		if err != nil {
   230  			return false, errors.Wrap(err, "error converting trust relationship to json")
   231  		}
   232  		policyInput := &iam.UpdateAssumeRolePolicyInput{
   233  			RoleName:       role.RoleName,
   234  			PolicyDocument: aws.String(trustRelationshipJSON),
   235  		}
   236  		updated = true
   237  		if _, err := s.IAMClient.UpdateAssumeRolePolicy(policyInput); err != nil {
   238  			return updated, err
   239  		}
   240  	}
   241  
   242  	tagInput := &iam.TagRoleInput{
   243  		RoleName: role.RoleName,
   244  	}
   245  	untagInput := &iam.UntagRoleInput{
   246  		RoleName: role.RoleName,
   247  	}
   248  	currentTags := make(map[string]string)
   249  	for _, tag := range role.Tags {
   250  		currentTags[*tag.Key] = *tag.Value
   251  		if *tag.Key == infrav1.ClusterAWSCloudProviderTagKey(key) {
   252  			continue
   253  		}
   254  		if _, ok := additionalTags[*tag.Key]; !ok {
   255  			untagInput.TagKeys = append(untagInput.TagKeys, tag.Key)
   256  		}
   257  	}
   258  	for key, value := range additionalTags {
   259  		if currentV, ok := currentTags[key]; !ok || value != currentV {
   260  			tagInput.Tags = append(tagInput.Tags, &iam.Tag{
   261  				Key:   aws.String(key),
   262  				Value: aws.String(value),
   263  			})
   264  		}
   265  	}
   266  
   267  	if len(tagInput.Tags) > 0 {
   268  		updated = true
   269  		_, err = s.IAMClient.TagRole(tagInput)
   270  		if err != nil {
   271  			return updated, err
   272  		}
   273  	}
   274  
   275  	if len(untagInput.TagKeys) > 0 {
   276  		updated = true
   277  		_, err = s.IAMClient.UntagRole(untagInput)
   278  		if err != nil {
   279  			return updated, err
   280  		}
   281  	}
   282  
   283  	return updated, nil
   284  }
   285  
   286  func (s *IAMService) detachAllPoliciesForRole(name string) error {
   287  	s.V(3).Info("Detaching all policies for role", "role", name)
   288  	input := &iam.ListAttachedRolePoliciesInput{
   289  		RoleName: &name,
   290  	}
   291  	policies, err := s.IAMClient.ListAttachedRolePolicies(input)
   292  	if err != nil {
   293  		return errors.Wrapf(err, "error fetching policies for role %s", name)
   294  	}
   295  	for _, p := range policies.AttachedPolicies {
   296  		s.V(2).Info("Detaching policy", "policy", *p)
   297  		if err := s.detachIAMRolePolicy(name, *p.PolicyArn); err != nil {
   298  			return err
   299  		}
   300  	}
   301  	return nil
   302  }
   303  
   304  // DeleteRole will delete a role from the IAMService.
   305  func (s *IAMService) DeleteRole(name string) error {
   306  	if err := s.detachAllPoliciesForRole(name); err != nil {
   307  		return errors.Wrapf(err, "error detaching policies for role %s", name)
   308  	}
   309  
   310  	input := &iam.DeleteRoleInput{
   311  		RoleName: aws.String(name),
   312  	}
   313  
   314  	if _, err := s.IAMClient.DeleteRole(input); err != nil {
   315  		return errors.Wrapf(err, "error deleting role %s", name)
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  // IsUnmanaged will check if a given role and tag are unmanaged against the IAMService.
   322  func (s *IAMService) IsUnmanaged(role *iam.Role, key string) bool {
   323  	keyToFind := infrav1.ClusterAWSCloudProviderTagKey(key)
   324  	for _, tag := range role.Tags {
   325  		if *tag.Key == keyToFind && *tag.Value == string(infrav1.ResourceLifecycleOwned) {
   326  			return false
   327  		}
   328  	}
   329  
   330  	return true
   331  }
   332  
   333  // ControlPlaneTrustRelationship will generate a ControlPlane PolicyDocument.
   334  func ControlPlaneTrustRelationship(enableFargate bool) *iamv1.PolicyDocument {
   335  	identity := make(iamv1.Principals)
   336  	identity["Service"] = []string{"eks.amazonaws.com"}
   337  	if enableFargate {
   338  		identity["Service"] = append(identity["Service"], EKSFargateService)
   339  	}
   340  
   341  	policy := &iamv1.PolicyDocument{
   342  		Version: "2012-10-17",
   343  		Statement: []iamv1.StatementEntry{
   344  			{
   345  				Effect: "Allow",
   346  				Action: []string{
   347  					"sts:AssumeRole",
   348  				},
   349  				Principal: identity,
   350  			},
   351  		},
   352  	}
   353  
   354  	return policy
   355  }
   356  
   357  // FargateTrustRelationship will generate a Fargate PolicyDocument.
   358  func FargateTrustRelationship() *iamv1.PolicyDocument {
   359  	identity := make(iamv1.Principals)
   360  	identity["Service"] = []string{EKSFargateService}
   361  
   362  	policy := &iamv1.PolicyDocument{
   363  		Version: "2012-10-17",
   364  		Statement: []iamv1.StatementEntry{
   365  			{
   366  				Effect: "Allow",
   367  				Action: []string{
   368  					"sts:AssumeRole",
   369  				},
   370  				Principal: identity,
   371  			},
   372  		},
   373  	}
   374  
   375  	return policy
   376  }
   377  
   378  // NodegroupTrustRelationship will generate a Nodegroup PolicyDocument.
   379  func NodegroupTrustRelationship() *iamv1.PolicyDocument {
   380  	identity := make(iamv1.Principals)
   381  	identity["Service"] = []string{"ec2.amazonaws.com"}
   382  
   383  	policy := &iamv1.PolicyDocument{
   384  		Version: "2012-10-17",
   385  		Statement: []iamv1.StatementEntry{
   386  			{
   387  				Effect: "Allow",
   388  				Action: []string{
   389  					"sts:AssumeRole",
   390  				},
   391  				Principal: identity,
   392  			},
   393  		},
   394  	}
   395  
   396  	return policy
   397  }
   398  
   399  func findStringInSlice(slice []*string, toFind string) bool {
   400  	for _, item := range slice {
   401  		if *item == toFind {
   402  			return true
   403  		}
   404  	}
   405  
   406  	return false
   407  }
   408  
   409  const stsAWSAudience = "sts.amazonaws.com"
   410  
   411  // CreateOIDCProvider will create an OIDC provider.
   412  func (s *IAMService) CreateOIDCProvider(cluster *eks.Cluster) (string, error) {
   413  	issuerURL, err := url.Parse(*cluster.Identity.Oidc.Issuer)
   414  	if err != nil {
   415  		return "", err
   416  	}
   417  	if issuerURL.Scheme != "https" {
   418  		return "", errors.Errorf("invalid scheme for issuer URL %s", issuerURL.String())
   419  	}
   420  
   421  	thumbprint, err := fetchRootCAThumbprint(issuerURL.String())
   422  	if err != nil {
   423  		return "", err
   424  	}
   425  	input := iam.CreateOpenIDConnectProviderInput{
   426  		ClientIDList:   aws.StringSlice([]string{stsAWSAudience}),
   427  		ThumbprintList: aws.StringSlice([]string{thumbprint}),
   428  		Url:            aws.String(issuerURL.String()),
   429  	}
   430  	provider, err := s.IAMClient.CreateOpenIDConnectProvider(&input)
   431  	if err != nil {
   432  		return "", errors.Wrap(err, "error creating provider")
   433  	}
   434  	return *provider.OpenIDConnectProviderArn, nil
   435  }
   436  
   437  func fetchRootCAThumbprint(issuerURL string) (string, error) {
   438  	response, err := http.Get(issuerURL)
   439  	if err != nil {
   440  		return "", err
   441  	}
   442  	defer response.Body.Close()
   443  
   444  	rootCA := response.TLS.PeerCertificates[len(response.TLS.PeerCertificates)-1]
   445  	sha1Sum := sha1.Sum(rootCA.Raw) //nolint:gosec
   446  	return hex.EncodeToString(sha1Sum[:]), nil
   447  }
   448  
   449  // DeleteOIDCProvider will delete an OIDC provider.
   450  func (s *IAMService) DeleteOIDCProvider(arn *string) error {
   451  	input := iam.DeleteOpenIDConnectProviderInput{
   452  		OpenIDConnectProviderArn: arn,
   453  	}
   454  
   455  	_, err := s.IAMClient.DeleteOpenIDConnectProvider(&input)
   456  	if err != nil {
   457  		return errors.Wrap(err, "error deleting provider")
   458  	}
   459  	return nil
   460  }