From eec24e08661bd1878235413b27726c4e48fb83b8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 2 Nov 2021 22:16:52 -0700 Subject: [PATCH] s3-credentials delete-user command, closes #10 --- README.md | 17 +++++++++++++++ s3_credentials/cli.py | 35 ++++++++++++++++++++++++++++++ tests/test_s3_credentials.py | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/README.md b/README.md index 1f27e88..41306f2 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,23 @@ You can pass any number of usernames here. If you don't specify a username the t s3-credentials list-user-policies +### delete-user + +In trying out this tool it's possible you will create several different user accounts that you later decide to clean up. + +Deleting AWS users is a little fiddly: you first need to delete their access keys, then their inline policies and finally the user themselves. + +The `s3-credentials delete-user` handles this for you: + +``` +% s3-credentials delete-user s3.read-write.simonw-test-bucket-10 +User: s3.read-write.simonw-test-bucket-10 + Deleted policy: s3.read-write.simonw-test-bucket-10 + Deleted access key: AKIAWXFXAIOZK3GPEIWR + Deleted user +``` +You can pass it multiple usernames to delete multiple users at a time. + ## Development To contribute to this tool, first checkout the code. Then create a new virtual environment: diff --git a/s3_credentials/cli.py b/s3_credentials/cli.py index 9a89078..51cfbd8 100644 --- a/s3_credentials/cli.py +++ b/s3_credentials/cli.py @@ -233,3 +233,38 @@ def list_buckets(array, nl): click.echo(json.dumps(bucket, indent=4, default=str)) if gathered: click.echo(json.dumps(gathered, indent=4, default=str)) + + +@cli.command() +@click.argument("usernames", nargs=-1, required=True) +def delete_user(usernames): + "Delete specified users, their access keys and their inline policies" + iam = boto3.client("iam") + policy_paginator = iam.get_paginator("list_user_policies") + access_key_paginator = iam.get_paginator("list_access_keys") + for username in usernames: + click.echo("User: {}".format(username)) + # Fetch and delete their policies + policy_names = [] + for response in policy_paginator.paginate(UserName=username): + for policy_name in response["PolicyNames"]: + policy_names.append(policy_name) + for policy_name in policy_names: + iam.delete_user_policy( + UserName=username, + PolicyName=policy_name, + ) + click.echo(" Deleted policy: {}".format(policy_name)) + # Fetch and delete their access keys + access_key_ids = [] + for response in access_key_paginator.paginate(UserName=username): + for access_key in response["AccessKeyMetadata"]: + access_key_ids.append(access_key["AccessKeyId"]) + for access_key_id in access_key_ids: + iam.delete_access_key( + UserName=username, + AccessKeyId=access_key_id, + ) + click.echo(" Deleted access key: {}".format(access_key_id)) + iam.delete_user(UserName=username) + click.echo(" Deleted user") diff --git a/tests/test_s3_credentials.py b/tests/test_s3_credentials.py index 1b85775..fff8d26 100644 --- a/tests/test_s3_credentials.py +++ b/tests/test_s3_credentials.py @@ -141,3 +141,44 @@ def test_list_user_policies(mocker): call().get_user_policy(UserName="two", PolicyName="policy-one"), call().get_user_policy(UserName="two", PolicyName="policy-two"), ] + + +def test_delete_user(mocker): + boto3 = mocker.patch("boto3.client") + boto3.return_value = Mock() + boto3.return_value.get_user_policy.return_value = { + "PolicyDocument": {"policy": "here"} + } + + def get_paginator(type): + m = Mock() + if type == "list_access_keys": + m.paginate.return_value = [ + {"AccessKeyMetadata": [{"AccessKeyId": "one"}, {"AccessKeyId": "two"}]} + ] + elif type == "list_user_policies": + m.paginate.return_value = [{"PolicyNames": ["policy-one"]}] + return m + + boto3().get_paginator.side_effect = get_paginator + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ["delete-user", "user-123"], catch_exceptions=False) + assert result.exit_code == 0 + assert result.output == ( + "User: user-123\n" + " Deleted policy: policy-one\n" + " Deleted access key: one\n" + " Deleted access key: two\n" + " Deleted user\n" + ) + assert boto3.mock_calls == [ + call(), + call("iam"), + call().get_paginator("list_user_policies"), + call().get_paginator("list_access_keys"), + call().delete_user_policy(UserName="user-123", PolicyName="policy-one"), + call().delete_access_key(UserName="user-123", AccessKeyId="one"), + call().delete_access_key(UserName="user-123", AccessKeyId="two"), + call().delete_user(UserName="user-123"), + ]