Skip to content

Commit 0d35d69

Browse files
[BB-1367] add delete iam user resource (NEW PERMISSIONS NEEDED) (#89)
* add delete iam user resource * add ci provisioning and deprovisioning test * use proper iamClient * add capabilities and config workflow
1 parent d3a88d7 commit 0d35d69

File tree

4 files changed

+308
-0
lines changed

4 files changed

+308
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Generate capabilities and config schema
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
generate_outputs:
10+
if: github.actor != 'github-actions[bot]'
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
with:
17+
token: ${{ secrets.RELENG_GITHUB_TOKEN }}
18+
19+
- name: Setup Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version-file: "go.mod"
23+
24+
- name: Build
25+
run: go build -o connector ./cmd/baton-aws
26+
27+
- name: Run and save config output
28+
run: ./connector config > config_schema.json
29+
30+
- name: Run and save capabilities output
31+
env:
32+
BATON_GLOBAL_AWS_SSO_REGION: us-east-1
33+
BATON_GLOBAL_REGION: us-east-1
34+
BATON_GLOBAL_ACCESS_KEY_ID: my-key-id
35+
BATON_GLOBAL_SECRET_ACCESS_KEY: my-secret-key
36+
run: ./connector capabilities > baton_capabilities.json
37+
38+
- name: Commit changes
39+
uses: EndBug/add-and-commit@v9
40+
with:
41+
default_author: github_actions
42+
message: "Updating baton config schema and capabilities."
43+
add: |
44+
config_schema.json
45+
baton_capabilities.json

.github/workflows/ci.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,12 @@ jobs:
6868
baton-entitlement: 'sso_group:arn:aws:identitystore:us-east-1::d-90679d1878/group/9458d408-40b1-709f-4f45-92be754928e5:member'
6969
baton-principal: 'arn:aws:identitystore:us-east-1::d-90679d1878/user/54982488-f0d1-70c1-1dd5-6db47f7add45'
7070
baton-principal-type: 'sso_user'
71+
- name: Test IAM user provisioning and deprovisioning
72+
uses: ConductorOne/github-workflows/actions/account-provisioning@v3
73+
with:
74+
connector: ./baton-aws
75+
account-email: '[email protected]'
76+
account-display-name: 'test-user-ci'
77+
account-profile: '{"username": "test-user-ci", "email": "[email protected]"}'
78+
account-type: 'iam_user'
79+
search-method: 'display_name'

baton_capabilities.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"@type": "type.googleapis.com/c1.connector.v2.ConnectorCapabilities",
3+
"resourceTypeCapabilities": [
4+
{
5+
"resourceType": {
6+
"id": "group",
7+
"displayName": "Group",
8+
"traits": [
9+
"TRAIT_GROUP"
10+
],
11+
"annotations": [
12+
{
13+
"@type": "type.googleapis.com/c1.connector.v2.V1Identifier",
14+
"id": "group"
15+
}
16+
]
17+
},
18+
"capabilities": [
19+
"CAPABILITY_SYNC",
20+
"CAPABILITY_PROVISION"
21+
]
22+
},
23+
{
24+
"resourceType": {
25+
"id": "iam_user",
26+
"displayName": "IAM User",
27+
"traits": [
28+
"TRAIT_USER"
29+
],
30+
"annotations": [
31+
{
32+
"@type": "type.googleapis.com/c1.connector.v2.SkipEntitlementsAndGrants"
33+
},
34+
{
35+
"@type": "type.googleapis.com/c1.connector.v2.V1Identifier",
36+
"id": "iam_user"
37+
}
38+
]
39+
},
40+
"capabilities": [
41+
"CAPABILITY_SYNC",
42+
"CAPABILITY_ACCOUNT_PROVISIONING",
43+
"CAPABILITY_RESOURCE_DELETE"
44+
]
45+
},
46+
{
47+
"resourceType": {
48+
"id": "role",
49+
"displayName": "IAM Role",
50+
"traits": [
51+
"TRAIT_ROLE"
52+
],
53+
"annotations": [
54+
{
55+
"@type": "type.googleapis.com/c1.connector.v2.V1Identifier",
56+
"id": "role"
57+
}
58+
]
59+
},
60+
"capabilities": [
61+
"CAPABILITY_SYNC"
62+
]
63+
}
64+
],
65+
"connectorCapabilities": [
66+
"CAPABILITY_PROVISION",
67+
"CAPABILITY_SYNC",
68+
"CAPABILITY_ACCOUNT_PROVISIONING",
69+
"CAPABILITY_RESOURCE_DELETE"
70+
],
71+
"credentialDetails": {
72+
"capabilityAccountProvisioning": {
73+
"supportedCredentialOptions": [
74+
"CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD"
75+
],
76+
"preferredCredentialOption": "CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD"
77+
}
78+
}
79+
}

pkg/connector/iam_user.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,178 @@ func (o *iamUserResourceType) CreateAccount(
288288
IsCreateAccountResult: true,
289289
}, nil, nil, nil
290290
}
291+
292+
func (o *iamUserResourceType) Delete(ctx context.Context, resourceId *v2.ResourceId, parentResourceID *v2.ResourceId) (annotations.Annotations, error) {
293+
var noSuchEntity *iamTypes.NoSuchEntityException
294+
l := ctxzap.Extract(ctx)
295+
if resourceId.ResourceType != resourceTypeIAMUser.Id {
296+
return nil, fmt.Errorf("aws-connector: only IAM user resources can be deleted")
297+
}
298+
userName, err := iamUserNameFromARN(resourceId.Resource)
299+
if err != nil {
300+
return nil, err
301+
}
302+
awsStringUserName := awsSdk.String(userName)
303+
304+
iamClient := o.iamClient
305+
if parentResourceID != nil {
306+
iamClient, err = o.awsClientFactory.GetIAMClient(ctx, parentResourceID.Resource)
307+
if err != nil {
308+
return nil, fmt.Errorf("aws-connector: GetIAMClient failed: %w", err)
309+
}
310+
}
311+
312+
// try to fetch the user, if not found then the user has already been deleted
313+
user, err := iamClient.GetUser(ctx, &iam.GetUserInput{UserName: awsStringUserName})
314+
if err != nil {
315+
if errors.As(err, &noSuchEntity) {
316+
l.Info("User not found, returning success for delete operation")
317+
return nil, nil
318+
}
319+
return nil, fmt.Errorf("aws-connector: iam.GetUser failed: %w", err)
320+
}
321+
322+
if user.User == nil {
323+
return nil, fmt.Errorf("aws-connector: user not found")
324+
}
325+
326+
// To delete a user through the API we'll need to manually delete information associated with it,
327+
// which is a 10 step process (9 + delete itself).
328+
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_remove.html#id_users_deleting_cli
329+
330+
// Permission needed: iam:DeleteLoginProfile
331+
_, err = iamClient.DeleteLoginProfile(ctx, &iam.DeleteLoginProfileInput{UserName: awsStringUserName})
332+
if err != nil {
333+
if !errors.As(err, &noSuchEntity) {
334+
return nil, fmt.Errorf("aws-connector: failed to delete login profile: %w", err)
335+
}
336+
l.Info("login profile not found, skipping")
337+
}
338+
339+
// Delete all access keys
340+
// Permission needed: iam:ListAccessKeys, iam:DeleteAccessKey
341+
keys, err := iamClient.ListAccessKeys(ctx, &iam.ListAccessKeysInput{UserName: awsStringUserName})
342+
if err != nil {
343+
return nil, fmt.Errorf("aws-connector: failed to list access keys: %w", err)
344+
}
345+
346+
for _, key := range keys.AccessKeyMetadata {
347+
_, err = iamClient.DeleteAccessKey(ctx, &iam.DeleteAccessKeyInput{UserName: awsStringUserName, AccessKeyId: awsSdk.String(awsSdk.ToString(key.AccessKeyId))})
348+
if err != nil {
349+
return nil, fmt.Errorf("aws-connector: failed to delete access key: %w", err)
350+
}
351+
}
352+
353+
// Delete all signing certificates
354+
// Permission needed: iam:ListSigningCertificates, iam:DeleteSigningCertificate
355+
certificates, err := iamClient.ListSigningCertificates(ctx, &iam.ListSigningCertificatesInput{UserName: awsStringUserName})
356+
if err != nil {
357+
return nil, fmt.Errorf("aws-connector: failed to list signing certificates: %w", err)
358+
}
359+
360+
for _, certificate := range certificates.Certificates {
361+
_, err = iamClient.DeleteSigningCertificate(ctx, &iam.DeleteSigningCertificateInput{UserName: awsStringUserName, CertificateId: awsSdk.String(awsSdk.ToString(certificate.CertificateId))})
362+
if err != nil {
363+
return nil, fmt.Errorf("aws-connector: failed to delete signing certificate: %w", err)
364+
}
365+
}
366+
367+
// Delete all SSH public keys
368+
// Permission needed: iam:ListSSHPublicKeys, iam:DeleteSSHPublicKey
369+
sshKeys, err := iamClient.ListSSHPublicKeys(ctx, &iam.ListSSHPublicKeysInput{UserName: awsStringUserName})
370+
if err != nil {
371+
return nil, fmt.Errorf("aws-connector: failed to list SSH public keys: %w", err)
372+
}
373+
374+
for _, key := range sshKeys.SSHPublicKeys {
375+
_, err = iamClient.DeleteSSHPublicKey(ctx, &iam.DeleteSSHPublicKeyInput{UserName: awsStringUserName, SSHPublicKeyId: awsSdk.String(awsSdk.ToString(key.SSHPublicKeyId))})
376+
if err != nil {
377+
return nil, fmt.Errorf("aws-connector: failed to delete SSH public key: %w", err)
378+
}
379+
}
380+
381+
// Delete all service specific credentials
382+
// Permission needed: iam:ListServiceSpecificCredentials, iam:DeleteServiceSpecificCredential
383+
ssCredentials, err := iamClient.ListServiceSpecificCredentials(ctx, &iam.ListServiceSpecificCredentialsInput{UserName: awsStringUserName})
384+
if err != nil {
385+
return nil, fmt.Errorf("aws-connector: failed to list service specific credentials: %w", err)
386+
}
387+
388+
for _, credential := range ssCredentials.ServiceSpecificCredentials {
389+
_, err = iamClient.DeleteServiceSpecificCredential(
390+
ctx,
391+
&iam.DeleteServiceSpecificCredentialInput{
392+
UserName: awsStringUserName,
393+
ServiceSpecificCredentialId: awsSdk.String(awsSdk.ToString(credential.ServiceSpecificCredentialId)),
394+
},
395+
)
396+
if err != nil {
397+
return nil, fmt.Errorf("aws-connector: failed to delete service specific credential: %w", err)
398+
}
399+
}
400+
401+
// If user has MFA, deactivate them
402+
// Permission needed: iam:ListMFADevices, iam:DeactivateMFADevice
403+
mfaDevices, err := iamClient.ListMFADevices(ctx, &iam.ListMFADevicesInput{UserName: awsStringUserName})
404+
if err != nil {
405+
return nil, fmt.Errorf("aws-connector: failed to list MFA devices: %w", err)
406+
}
407+
408+
for _, device := range mfaDevices.MFADevices {
409+
_, err = iamClient.DeactivateMFADevice(ctx, &iam.DeactivateMFADeviceInput{UserName: awsStringUserName, SerialNumber: awsSdk.String(awsSdk.ToString(device.SerialNumber))})
410+
if err != nil {
411+
return nil, fmt.Errorf("aws-connector: failed to deactivate MFA device: %w", err)
412+
}
413+
}
414+
415+
// Delete users inline policies
416+
// Permission needed: iam:ListUserPolicies, iam:DeleteUserPolicy
417+
userPolicies, err := iamClient.ListUserPolicies(ctx, &iam.ListUserPoliciesInput{UserName: awsStringUserName})
418+
if err != nil {
419+
return nil, fmt.Errorf("aws-connector: failed to list user policies: %w", err)
420+
}
421+
422+
for _, policy := range userPolicies.PolicyNames {
423+
_, err = iamClient.DeleteUserPolicy(ctx, &iam.DeleteUserPolicyInput{UserName: awsStringUserName, PolicyName: awsSdk.String(policy)})
424+
if err != nil {
425+
return nil, fmt.Errorf("aws-connector: failed to delete user policy: %w", err)
426+
}
427+
}
428+
429+
// List and detach all attached policies
430+
// Permission needed: iam:ListAttachedUserPolicies, iam:DetachUserPolicy
431+
attachedPolicies, err := iamClient.ListAttachedUserPolicies(ctx, &iam.ListAttachedUserPoliciesInput{UserName: awsStringUserName})
432+
if err != nil {
433+
return nil, fmt.Errorf("aws-connector: failed to list attached user policies: %w", err)
434+
}
435+
436+
for _, policy := range attachedPolicies.AttachedPolicies {
437+
_, err = iamClient.DetachUserPolicy(ctx, &iam.DetachUserPolicyInput{UserName: awsStringUserName, PolicyArn: awsSdk.String(awsSdk.ToString(policy.PolicyArn))})
438+
if err != nil {
439+
return nil, fmt.Errorf("aws-connector: failed to detach user policy: %w", err)
440+
}
441+
}
442+
443+
// Remove the user from any IAM groups
444+
// Permission needed: iam:ListGroupsForUser, iam:RemoveUserFromGroup
445+
userGroups, err := iamClient.ListGroupsForUser(ctx, &iam.ListGroupsForUserInput{UserName: awsStringUserName})
446+
if err != nil {
447+
return nil, fmt.Errorf("aws-connector: failed to list groups for user: %w", err)
448+
}
449+
450+
for _, group := range userGroups.Groups {
451+
_, err = iamClient.RemoveUserFromGroup(ctx, &iam.RemoveUserFromGroupInput{UserName: awsStringUserName, GroupName: awsSdk.String(awsSdk.ToString(group.GroupName))})
452+
if err != nil {
453+
return nil, fmt.Errorf("aws-connector: failed to remove user from group: %w", err)
454+
}
455+
}
456+
457+
// Proceed to delete the user
458+
// Permission needed: iam:DeleteUser
459+
_, err = iamClient.DeleteUser(ctx, &iam.DeleteUserInput{UserName: awsStringUserName})
460+
if err != nil {
461+
return nil, fmt.Errorf("aws-connector: failed to delete user: %w", err)
462+
}
463+
464+
return nil, nil
465+
}

0 commit comments

Comments
 (0)