Skip to content

Commit ef4faf9

Browse files
authored
Merge pull request #773 from ekristen/ssm-quicksetup-setting
feat(ssm-quicksetup): adds create role setting to allow cleanup when cloudformation is nuked
2 parents e1c4602 + bb6ff24 commit ef4faf9

File tree

3 files changed

+327
-6
lines changed

3 files changed

+327
-6
lines changed

docs/resources/inspector-2.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Inspector2
1616

1717
- `AccountID`: No Description
1818
- `ResourceState`: No Description
19+
- `Status`: No Description
1920

2021
!!! note - Using Properties
2122
Properties are what [Filters](../config-filtering.md) are written against in your configuration. You use the property

docs/resources/ssm-quick-setup-configuration-manager.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,18 @@ the filter.
2929

3030
The string value is always what is used in the output of the log format when a resource is identified.
3131

32+
## Settings
33+
34+
- `CreateRoleToDelete`
35+
36+
37+
### CreateRoleToDelete
38+
39+
!!! note
40+
There is currently no description for this setting. Often times settings are fairly self-explanatory. However, we
41+
are working on adding descriptions for all settings.
42+
43+
```text
44+
CreateRoleToDelete
45+
```
46+

resources/ssmquicksetup-configuration-manager.go

Lines changed: 311 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@ package resources
22

33
import (
44
"context"
5+
"encoding/json"
6+
"fmt"
57
"strings"
68

79
"github.com/gotidy/ptr"
810

11+
"github.com/aws/aws-sdk-go-v2/aws"
12+
"github.com/aws/aws-sdk-go-v2/service/iam"
13+
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
914
"github.com/aws/aws-sdk-go-v2/service/ssmquicksetup"
15+
"github.com/aws/aws-sdk-go-v2/service/sts"
1016

1117
"github.com/ekristen/libnuke/pkg/registry"
1218
"github.com/ekristen/libnuke/pkg/resource"
19+
"github.com/ekristen/libnuke/pkg/settings"
1320
"github.com/ekristen/libnuke/pkg/types"
1421

1522
"github.com/ekristen/aws-nuke/v3/pkg/nuke"
@@ -23,6 +30,9 @@ func init() {
2330
Scope: nuke.Account,
2431
Resource: &SSMQuickSetupConfigurationManager{},
2532
Lister: &SSMQuickSetupConfigurationManagerLister{},
33+
Settings: []string{
34+
"CreateRoleToDelete",
35+
},
2636
})
2737
}
2838

@@ -40,19 +50,26 @@ func (l *SSMQuickSetupConfigurationManagerLister) List(ctx context.Context, o in
4050

4151
for _, p := range res.ConfigurationManagersList {
4252
resources = append(resources, &SSMQuickSetupConfigurationManager{
43-
svc: svc,
44-
ARN: p.ManagerArn,
45-
Name: p.Name,
53+
svc: svc,
54+
iamSvc: iam.NewFromConfig(*opts.Config),
55+
stsSvc: sts.NewFromConfig(*opts.Config),
56+
ARN: p.ManagerArn,
57+
Name: p.Name,
58+
createdRoles: make(map[string]bool), // Track which roles we created
4659
})
4760
}
4861

4962
return resources, nil
5063
}
5164

5265
type SSMQuickSetupConfigurationManager struct {
53-
svc *ssmquicksetup.Client
54-
ARN *string
55-
Name *string
66+
svc *ssmquicksetup.Client
67+
iamSvc *iam.Client
68+
stsSvc *sts.Client
69+
ARN *string
70+
Name *string
71+
settings *settings.Setting
72+
createdRoles map[string]bool // Track roles created during deletion process
5673
}
5774

5875
// GetName returns the name of the resource or the last part of the ARN if not set so that the stringer resource has
@@ -66,10 +83,43 @@ func (r *SSMQuickSetupConfigurationManager) GetName() string {
6683
return parts[len(parts)-1]
6784
}
6885

86+
func (r *SSMQuickSetupConfigurationManager) Settings(setting *settings.Setting) {
87+
r.settings = setting
88+
}
89+
6990
func (r *SSMQuickSetupConfigurationManager) Remove(ctx context.Context) error {
7091
_, err := r.svc.DeleteConfigurationManager(ctx, &ssmquicksetup.DeleteConfigurationManagerInput{
7192
ManagerArn: r.ARN,
7293
})
94+
95+
// Check if we got the specific error about role access and if CreateRoleToDelete setting is enabled
96+
if err != nil && r.settings.GetBool("CreateRoleToDelete") {
97+
if roleName := r.extractRoleNameFromError(err); roleName != "" {
98+
// Initialize createdRoles map if nil
99+
if r.createdRoles == nil {
100+
r.createdRoles = make(map[string]bool)
101+
}
102+
103+
// Create the specific role mentioned in the error message
104+
if createErr := r.createRoleFromError(ctx, roleName); createErr != nil {
105+
return createErr
106+
}
107+
108+
// Mark this role as created by us
109+
r.createdRoles[roleName] = true
110+
111+
// Retry the deletion after creating role
112+
_, err = r.svc.DeleteConfigurationManager(ctx, &ssmquicksetup.DeleteConfigurationManagerInput{
113+
ManagerArn: r.ARN,
114+
})
115+
}
116+
}
117+
118+
// If deletion was successful and we created roles, clean them up
119+
if err == nil && len(r.createdRoles) > 0 {
120+
r.cleanupCreatedRoles(ctx)
121+
}
122+
73123
return err
74124
}
75125

@@ -80,3 +130,258 @@ func (r *SSMQuickSetupConfigurationManager) Properties() types.Properties {
80130
func (r *SSMQuickSetupConfigurationManager) String() string {
81131
return r.GetName()
82132
}
133+
134+
// cleanupCreatedRoles removes all roles that were created during the deletion process
135+
func (r *SSMQuickSetupConfigurationManager) cleanupCreatedRoles(ctx context.Context) {
136+
// Clean up roles in order: execution roles first, then admin roles
137+
// This ensures we don't have dependency issues
138+
var adminRoles []string
139+
var execRoles []string
140+
141+
for roleName := range r.createdRoles {
142+
if strings.Contains(roleName, "Administration") {
143+
adminRoles = append(adminRoles, roleName)
144+
} else if strings.Contains(roleName, "Execution") {
145+
execRoles = append(execRoles, roleName)
146+
}
147+
}
148+
149+
// Clean up execution roles first
150+
for _, roleName := range execRoles {
151+
if err := r.cleanupRole(ctx, roleName, false); err != nil {
152+
// Log the error but don't fail the overall operation
153+
// The main resource has been successfully deleted
154+
continue
155+
}
156+
}
157+
158+
// Clean up admin roles last
159+
for _, roleName := range adminRoles {
160+
if err := r.cleanupRole(ctx, roleName, true); err != nil {
161+
// Log the error but don't fail the overall operation
162+
// The main resource has been successfully deleted
163+
continue
164+
}
165+
}
166+
}
167+
168+
// cleanupRole removes a specific role and its associated policies
169+
func (r *SSMQuickSetupConfigurationManager) cleanupRole(ctx context.Context, roleName string, isAdminRole bool) error {
170+
// For admin roles, remove the inline policy first
171+
if isAdminRole {
172+
_, err := r.iamSvc.DeleteRolePolicy(ctx, &iam.DeleteRolePolicyInput{
173+
RoleName: aws.String(roleName),
174+
PolicyName: aws.String("AssumeLocalExecutionRole"),
175+
})
176+
if err != nil {
177+
return err
178+
}
179+
} else {
180+
// For execution roles, detach the managed policy
181+
policyArn := "arn:aws:iam::aws:policy/AWSQuickSetupDeploymentRolePolicy"
182+
_, err := r.iamSvc.DetachRolePolicy(ctx, &iam.DetachRolePolicyInput{
183+
RoleName: aws.String(roleName),
184+
PolicyArn: aws.String(policyArn),
185+
})
186+
if err != nil {
187+
return err
188+
}
189+
}
190+
191+
// Delete the role
192+
_, err := r.iamSvc.DeleteRole(ctx, &iam.DeleteRoleInput{
193+
RoleName: aws.String(roleName),
194+
})
195+
196+
return err
197+
}
198+
199+
// extractRoleNameFromError extracts the role name from the error message
200+
func (r *SSMQuickSetupConfigurationManager) extractRoleNameFromError(err error) string {
201+
errStr := err.Error()
202+
203+
// Look for the pattern "Role ROLE_NAME can't be accessed"
204+
if strings.Contains(errStr, "can't be accessed") {
205+
// Find "Role " and extract the role name after it
206+
roleIndex := strings.Index(errStr, "Role ")
207+
if roleIndex != -1 {
208+
roleStart := roleIndex + 5 // Length of "Role "
209+
roleEnd := strings.Index(errStr[roleStart:], " can't be accessed")
210+
if roleEnd != -1 {
211+
return errStr[roleStart : roleStart+roleEnd]
212+
}
213+
}
214+
}
215+
216+
return ""
217+
}
218+
219+
// createRoleFromError creates the specific role mentioned in the error message
220+
func (r *SSMQuickSetupConfigurationManager) createRoleFromError(ctx context.Context, roleName string) error {
221+
// Get current account ID
222+
callerIdentity, err := r.stsSvc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
223+
if err != nil {
224+
return err
225+
}
226+
accountID := *callerIdentity.Account
227+
228+
// Determine which type of role to create based on the role name
229+
if strings.Contains(roleName, "Administration") {
230+
return r.createAdminRole(ctx, roleName, accountID)
231+
} else if strings.Contains(roleName, "Execution") {
232+
return r.createExecRole(ctx, roleName, accountID)
233+
}
234+
235+
return nil
236+
}
237+
238+
// createAdminRole creates the LocalAdministrationRole with CloudFormation trust policy
239+
func (r *SSMQuickSetupConfigurationManager) createAdminRole(ctx context.Context, roleName, accountID string) error {
240+
// Define trust policy for CloudFormation service with conditions
241+
trustPolicy := map[string]interface{}{
242+
"Version": "2012-10-17",
243+
"Statement": []map[string]interface{}{
244+
{
245+
"Effect": "Allow",
246+
"Principal": map[string]interface{}{
247+
"Service": "cloudformation.amazonaws.com",
248+
},
249+
"Action": "sts:AssumeRole",
250+
"Condition": map[string]interface{}{
251+
"StringEquals": map[string]interface{}{
252+
"aws:SourceAccount": accountID,
253+
},
254+
"StringLike": map[string]interface{}{
255+
"aws:SourceArn": fmt.Sprintf("arn:aws:cloudformation:*:%s:stackset/AWS-QuickSetup-*", accountID),
256+
},
257+
},
258+
},
259+
},
260+
}
261+
262+
trustPolicyJSON, err := json.Marshal(trustPolicy)
263+
if err != nil {
264+
return err
265+
}
266+
267+
// Create the role
268+
_, err = r.iamSvc.CreateRole(ctx, &iam.CreateRoleInput{
269+
RoleName: aws.String(roleName),
270+
AssumeRolePolicyDocument: aws.String(string(trustPolicyJSON)),
271+
Description: aws.String("LocalAdministrationRole created by aws-nuke for SSM QuickSetup Configuration Manager deletion"),
272+
Path: aws.String("/"),
273+
Tags: []iamtypes.Tag{
274+
{
275+
Key: aws.String("CreatedBy"),
276+
Value: aws.String("aws-nuke"),
277+
},
278+
{
279+
Key: aws.String("Purpose"),
280+
Value: aws.String("SSMQuickSetupConfigurationManager-Deletion"),
281+
},
282+
{
283+
Key: aws.String("ConfigurationManager"),
284+
Value: aws.String(r.GetName()),
285+
},
286+
},
287+
})
288+
289+
if err != nil {
290+
return err
291+
}
292+
293+
// Create inline policy for assuming execution roles (both LocalExecutionRole and LocalDeploymentExecutionRole)
294+
execRoleBaseName := strings.Replace(roleName, "LocalAdministrationRole", "", 1)
295+
policyDocument := map[string]interface{}{
296+
"Version": "2012-10-17",
297+
"Statement": []map[string]interface{}{
298+
{
299+
"Action": []string{"sts:AssumeRole"},
300+
"Resource": []string{
301+
fmt.Sprintf("arn:aws:iam::%s:role/%sLocalExecutionRole", accountID, execRoleBaseName),
302+
fmt.Sprintf("arn:aws:iam::%s:role/%sLocalDeploymentExecutionRole", accountID, execRoleBaseName),
303+
},
304+
"Effect": "Allow",
305+
},
306+
},
307+
}
308+
309+
policyJSON, err := json.Marshal(policyDocument)
310+
if err != nil {
311+
return err
312+
}
313+
314+
// Attach inline policy
315+
_, err = r.iamSvc.PutRolePolicy(ctx, &iam.PutRolePolicyInput{
316+
RoleName: aws.String(roleName),
317+
PolicyName: aws.String("AssumeLocalExecutionRole"),
318+
PolicyDocument: aws.String(string(policyJSON)),
319+
})
320+
321+
return err
322+
}
323+
324+
// createExecRole creates the LocalExecutionRole/LocalDeploymentExecutionRole with LocalAdministrationRole trust policy
325+
func (r *SSMQuickSetupConfigurationManager) createExecRole(ctx context.Context, roleName, accountID string) error {
326+
// Derive the corresponding admin role name from the execution role name
327+
var adminRoleName string
328+
if strings.Contains(roleName, "LocalExecutionRole") {
329+
adminRoleName = strings.Replace(roleName, "LocalExecutionRole", "LocalAdministrationRole", 1)
330+
} else if strings.Contains(roleName, "LocalDeploymentExecutionRole") {
331+
adminRoleName = strings.Replace(roleName, "LocalDeploymentExecutionRole", "LocalAdministrationRole", 1)
332+
}
333+
334+
// Define trust policy for LocalAdministrationRole
335+
trustPolicy := map[string]interface{}{
336+
"Version": "2012-10-17",
337+
"Statement": []map[string]interface{}{
338+
{
339+
"Effect": "Allow",
340+
"Principal": map[string]interface{}{
341+
"AWS": fmt.Sprintf("arn:aws:iam::%s:role/%s", accountID, adminRoleName),
342+
},
343+
"Action": "sts:AssumeRole",
344+
},
345+
},
346+
}
347+
348+
trustPolicyJSON, err := json.Marshal(trustPolicy)
349+
if err != nil {
350+
return err
351+
}
352+
353+
// Create the role
354+
_, err = r.iamSvc.CreateRole(ctx, &iam.CreateRoleInput{
355+
RoleName: aws.String(roleName),
356+
AssumeRolePolicyDocument: aws.String(string(trustPolicyJSON)),
357+
Description: aws.String("LocalExecutionRole created by aws-nuke for SSM QuickSetup Configuration Manager deletion"),
358+
Path: aws.String("/"),
359+
Tags: []iamtypes.Tag{
360+
{
361+
Key: aws.String("Managed"),
362+
Value: aws.String("aws-nuke"),
363+
},
364+
{
365+
Key: aws.String("Purpose"),
366+
Value: aws.String("SSMQuickSetupConfigurationManager-Deletion"),
367+
},
368+
{
369+
Key: aws.String("ConfigurationManager"),
370+
Value: aws.String(r.GetName()),
371+
},
372+
},
373+
})
374+
375+
if err != nil {
376+
return err
377+
}
378+
379+
// Attach the AWSQuickSetupDeploymentRolePolicy
380+
policyArn := "arn:aws:iam::aws:policy/AWSQuickSetupDeploymentRolePolicy"
381+
_, err = r.iamSvc.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{
382+
RoleName: aws.String(roleName),
383+
PolicyArn: aws.String(policyArn),
384+
})
385+
386+
return err
387+
}

0 commit comments

Comments
 (0)