Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions internal/pkg/aws/iam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type api interface {
ListRolePolicies(input *iam.ListRolePoliciesInput) (*iam.ListRolePoliciesOutput, error)
DeleteRole(input *iam.DeleteRoleInput) (*iam.DeleteRoleOutput, error)
CreateServiceLinkedRole(input *iam.CreateServiceLinkedRoleInput) (*iam.CreateServiceLinkedRoleOutput, error)
ListPolicies(input *iam.ListPoliciesInput) (*iam.ListPoliciesOutput, error)
}

// IAM wraps the AWS SDK's IAM client.
Expand Down Expand Up @@ -98,6 +99,32 @@ func (c *IAM) CreateECSServiceLinkedRole() error {
return nil
}

// ListPolicyNames returns a list of local policy names.
func (c *IAM) ListPolicyNames() ([]string, error) {
var policies []*iam.Policy
var marker *string
for {
output, err := c.client.ListPolicies(&iam.ListPoliciesInput{
Marker: marker,
Scope: aws.String("Local"),
Comment thread
huanjani marked this conversation as resolved.
PolicyUsageFilter: aws.String("PermissionsBoundary"),
})
if err != nil {
return nil, fmt.Errorf("list IAM policies: %w", err)
}
policies = append(policies, output.Policies...)
if !aws.BoolValue(output.IsTruncated) {
break
}
marker = output.Marker
}
var policyNames = make([]string, len(policies))
for i, policy := range policies {
policyNames[i] = aws.StringValue(policy.PolicyName)
}
return policyNames, nil
}

func (c *IAM) deleteRolePolicies(roleName string) error {
policyNames, err := c.listRolePolicyNames(roleName)
if err != nil {
Expand Down
61 changes: 61 additions & 0 deletions internal/pkg/aws/iam/iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,64 @@ func TestIAM_CreateECSServiceLinkedRole(t *testing.T) {
})
}
}

func TestIAM_ListPolicies(t *testing.T) {
testCases := map[string]struct {
inClient func(ctrl *gomock.Controller) *mocks.Mockapi

wantedPolicies []string
wantedErr error
}{
"wraps error on failure": {
inClient: func(ctrl *gomock.Controller) *mocks.Mockapi {
m := mocks.NewMockapi(ctrl)
m.EXPECT().
ListPolicies(gomock.Any()).
Return(nil, errors.New("some error"))
return m
},

wantedErr: errors.New("list IAM policies: some error"),
},
"returns list of policy names": {
inClient: func(ctrl *gomock.Controller) *mocks.Mockapi {
m := mocks.NewMockapi(ctrl)
m.EXPECT().
ListPolicies(gomock.Any()).
Return(&iam.ListPoliciesOutput{
Policies: []*iam.Policy{
{
PolicyName: aws.String("myFirstPolicyName"),
},
{
PolicyName: aws.String("mySecondPolicyName"),
},
},
}, nil)
return m
},
wantedPolicies: []string{"myFirstPolicyName", "mySecondPolicyName"},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// GIVEN
ctrl := gomock.NewController(t)
defer ctrl.Finish()
iam := &IAM{
client: tc.inClient(ctrl),
}

// WHEN
output, err := iam.ListPolicyNames()

// THEN
if tc.wantedErr != nil {
require.EqualError(t, err, tc.wantedErr.Error())
} else {
require.NoError(t, err)
require.Equal(t, tc.wantedPolicies, output)
}
})
}
}
15 changes: 15 additions & 0 deletions internal/pkg/aws/iam/mocks/mock_iam.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions internal/pkg/cli/app_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cli
import (
"errors"
"fmt"
"github.com/aws/copilot-cli/internal/pkg/aws/iam"
"os"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -51,6 +52,7 @@ type initAppOpts struct {
cfn appDeployer
prompt prompter
prog progress
iam policyLister
isSessionFromEnvVars func() (bool, error)

cachedHostedZoneID string
Expand All @@ -77,6 +79,7 @@ func newInitAppOpts(vars initAppVars) (*initAppOpts, error) {
cfn: cloudformation.New(sess, cloudformation.WithProgressTracker(os.Stderr)),
prompt: prompt.New(),
prog: termprogress.NewSpinner(log.DiagnosticWriter),
iam: iam.New(sess),
isSessionFromEnvVars: func() (bool, error) {
return sessions.AreCredsFromEnvVars(sess)
},
Expand All @@ -90,6 +93,11 @@ func (o *initAppOpts) Validate() error {
return err
}
}
if o.permissionsBoundary != "" {
if err := o.validatePermBound(o.permissionsBoundary); err != nil {
return err
}
}
if o.domainName != "" {
if err := validateDomainName(o.domainName); err != nil {
return fmt.Errorf("domain name %s is invalid: %w", o.domainName, err)
Expand Down Expand Up @@ -234,6 +242,19 @@ func (o *initAppOpts) validateAppName(name string) error {
return nil
}

func (o *initAppOpts) validatePermBound(policyName string) error {
IAMPolicies, err := o.iam.ListPolicyNames()
if err != nil {
return fmt.Errorf("list permissions boundary policies: %w", err)
}
for _, policy := range IAMPolicies {
if policy == policyName {
return nil
}
}
return fmt.Errorf("IAM policy %q not found in this account", policyName)
}

func (o *initAppOpts) isDomainOwned() error {
err := o.domainInfoGetter.IsRegisteredDomain(o.domainName)
if err == nil {
Expand Down
28 changes: 24 additions & 4 deletions internal/pkg/cli/app_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ type initAppMocks struct {
mockRoute53Svc *mocks.MockdomainHostedZoneGetter
mockStore *mocks.Mockstore
mockDomainInfoGetter *mocks.MockdomainInfoGetter
mockPolicyLister *mocks.MockpolicyLister
}

func TestInitAppOpts_Validate(t *testing.T) {
testCases := map[string]struct {
inAppName string
inDomainName string
inAppName string
inDomainName string
inPBPolicyName string

mock func(m *initAppMocks)

Expand Down Expand Up @@ -90,6 +92,21 @@ func TestInitAppOpts_Validate(t *testing.T) {
},
wantedError: errors.New("check if domain is owned by the account: some error"),
},
"wrap error from ListPolicies": {
inPBPolicyName: "nonexistentPolicyName",
mock: func(m *initAppMocks) {
m.mockPolicyLister.EXPECT().ListPolicyNames().Return(nil, errors.New("some error"))
},
wantedError: errors.New("list permissions boundary policies: some error"),
},
"invalid permissions boundary policy name": {
inPBPolicyName: "nonexistentPolicyName",
mock: func(m *initAppMocks) {
m.mockPolicyLister.EXPECT().ListPolicyNames().Return(
[]string{"existentPolicyName"}, nil)
},
wantedError: errors.New("IAM policy \"nonexistentPolicyName\" not found in this account"),
},
"invalid domain name that doesn't have a hosted zone": {
inDomainName: "badMockDomain.com",
mock: func(m *initAppMocks) {
Expand Down Expand Up @@ -132,16 +149,19 @@ func TestInitAppOpts_Validate(t *testing.T) {
mockStore: mocks.NewMockstore(ctrl),
mockRoute53Svc: mocks.NewMockdomainHostedZoneGetter(ctrl),
mockDomainInfoGetter: mocks.NewMockdomainInfoGetter(ctrl),
mockPolicyLister: mocks.NewMockpolicyLister(ctrl),
}
tc.mock(m)

opts := &initAppOpts{
route53: m.mockRoute53Svc,
domainInfoGetter: m.mockDomainInfoGetter,
store: m.mockStore,
iam: m.mockPolicyLister,
initAppVars: initAppVars{
name: tc.inAppName,
domainName: tc.inDomainName,
name: tc.inAppName,
domainName: tc.inDomainName,
permissionsBoundary: tc.inPBPolicyName,
},
}

Expand Down
4 changes: 4 additions & 0 deletions internal/pkg/cli/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,10 @@ type roleDeleter interface {
DeleteRole(string) error
}

type policyLister interface {
ListPolicyNames() ([]string, error)
}

type serviceDescriber interface {
DescribeService(app, env, svc string) (*ecs.ServiceDesc, error)
}
Expand Down
38 changes: 38 additions & 0 deletions internal/pkg/cli/mocks/mock_interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.