internal/resources/providers/awslib/iam/policy.go (176 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 iam import ( "context" "encoding/json" "fmt" "net/url" "github.com/aws/aws-sdk-go-v2/aws" iamsdk "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/elastic/cloudbeat/internal/resources/fetching" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" "github.com/elastic/cloudbeat/internal/resources/utils/pointers" ) const awsSupportAccessArn = "arn:aws:iam::aws:policy/AWSSupportAccess" func (p Provider) GetPolicies(ctx context.Context) ([]awslib.AwsResource, error) { policies, err := p.getPolicies(ctx) if err != nil { return nil, err } supportPolicy, err := p.getSupportPolicy(ctx) if err != nil { return nil, err } return append(policies, supportPolicy), nil } func (p Provider) getPolicies(ctx context.Context) ([]awslib.AwsResource, error) { var policies []awslib.AwsResource input := &iamsdk.ListPoliciesInput{OnlyAttached: true} for { listPoliciesOutput, err := p.client.ListPolicies(ctx, input) if err != nil { return nil, err } for _, policy := range listPoliciesOutput.Policies { if pointers.Deref(policy.Arn) == awsSupportAccessArn { // Fetch this one explicitly with getSupportPolicy(). // The reasoning is that we want to attach roles to the AWS support access policy. If we don't skip it // here, we will produce it another time in getSupportPolicy(), leading to duplicated resources. We // cannot just fetch the roles here either because if the AWS support access policy is not attached, // we will never see it. // See: https://github.com/elastic/cloudbeat/pull/900 continue } doc, err := p.getPolicyDocument(ctx, policy) if err != nil { return nil, err } policies = append(policies, Policy{ Policy: policy, Document: doc, }) } if !listPoliciesOutput.IsTruncated { break } input.Marker = listPoliciesOutput.Marker } return policies, nil } func (p Provider) getSupportPolicy(ctx context.Context) (awslib.AwsResource, error) { policy, err := p.client.GetPolicy(ctx, &iamsdk.GetPolicyInput{PolicyArn: aws.String(awsSupportAccessArn)}) if err != nil { return nil, err } doc, err := p.getPolicyDocument(ctx, *policy.Policy) if err != nil { return nil, err } awsSupportAccessPolicy := Policy{ Policy: *policy.Policy, Document: doc, Roles: make([]types.PolicyRole, 0), } input := &iamsdk.ListEntitiesForPolicyInput{ PolicyArn: aws.String(awsSupportAccessArn), EntityFilter: types.EntityTypeRole, } for { output, err := p.client.ListEntitiesForPolicy(ctx, input) if err != nil { return nil, err } awsSupportAccessPolicy.Roles = append(awsSupportAccessPolicy.Roles, output.PolicyRoles...) if !output.IsTruncated { break } input.Marker = output.Marker } return awsSupportAccessPolicy, nil } func (p Provider) getPolicyDocument(ctx context.Context, policy types.Policy) (map[string]any, error) { if policy.Arn == nil || policy.DefaultVersionId == nil { return nil, fmt.Errorf("invalid policy: %v", policy) } out, err := p.client.GetPolicyVersion(ctx, &iamsdk.GetPolicyVersionInput{PolicyArn: policy.Arn, VersionId: policy.DefaultVersionId}) if err != nil { return nil, err } doc, err := decodePolicyDocument(out.PolicyVersion) if err != nil { return nil, err } return doc, nil } func decodePolicyDocument(policyVersion *types.PolicyVersion) (map[string]any, error) { if policyVersion == nil || policyVersion.Document == nil { return nil, fmt.Errorf("invalid policy version: %v", policyVersion) } // The policy document is URL-encoded, compliant with RFC 3986 docString, err := url.QueryUnescape(*policyVersion.Document) if err != nil { return nil, fmt.Errorf("failed to unescape policy document: %w", err) } var doc map[string]any err = json.Unmarshal([]byte(docString), &doc) if err != nil { return nil, fmt.Errorf("failed to unmarshal policy document: %w", err) } return doc, nil } func (p Policy) GetResourceArn() string { return pointers.Deref(p.Arn) } func (p Policy) GetResourceName() string { return pointers.Deref(p.PolicyName) } func (p Policy) GetResourceType() string { return fetching.PolicyType } func (p Policy) GetRegion() string { return awslib.GlobalRegion } func (p Provider) listAttachedPolicies(ctx context.Context, identity *string) ([]types.AttachedPolicy, error) { p.log.Debugf("listAttachedPolicies for user: %s", *identity) input := &iamsdk.ListAttachedUserPoliciesInput{UserName: identity} var policies []types.AttachedPolicy for { output, err := p.client.ListAttachedUserPolicies(ctx, input) if err != nil { return []types.AttachedPolicy{}, err } policies = append(policies, output.AttachedPolicies...) if !output.IsTruncated { break } input.Marker = output.Marker } p.log.Debugf("attached policies for user: %s, policies: %v", *identity, policies) return policies, nil } func (p Provider) listInlinePolicies(ctx context.Context, identity *string) ([]PolicyDocument, error) { p.log.Debugf("listInlinePolicies for user: %s", *identity) input := &iamsdk.ListUserPoliciesInput{ UserName: identity, } var policyNames []string for { output, err := p.client.ListUserPolicies(ctx, input) if err != nil { return []PolicyDocument{}, err } policyNames = append(policyNames, output.PolicyNames...) if !output.IsTruncated { break } input.Marker = output.Marker } policies := make([]PolicyDocument, 0, len(policyNames)) for i := range policyNames { inlinePolicy, err := p.client.GetUserPolicy(ctx, &iamsdk.GetUserPolicyInput{ PolicyName: &policyNames[i], UserName: identity, }) if err != nil { p.log.Errorf("fail to get inline policy for user: %s, policy name: %s", *identity, policyNames[i]) policies = append(policies, PolicyDocument{PolicyName: policyNames[i]}) continue } policies = append(policies, PolicyDocument{PolicyName: policyNames[i], Policy: *inlinePolicy.PolicyDocument}) } p.log.Debugf("inline policies for user: %s, policies: %v", *identity, policies) return policies, nil }