Skip to content

Commit

Permalink
list-roles command
Browse files Browse the repository at this point in the history
* list-roles command, closes #61
* Refactor list-roles to use paginate() from #63
* Tests for list-roles - a whole lot of mocks
* Documentation for list-roles
* Fixed JSON output in --csv and --tsv
  • Loading branch information
simonw authored Jan 19, 2022
1 parent fc1e06c commit 7fb4db1
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 1 deletion.
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,123 @@ You can pass any number of usernames here. If you don't specify a username the t

s3-credentials list-user-policies

### list-roles

The `list-roles` command lists all of the roles available for the authenticated account.

Add `--details` to fetch the inline and attached managed policies for each row as well - this is slower as it needs to make several additional API calls for each role.

You can optionally add one or more role names to the command to display and fetch details about just those specific roles.

Example usage:

```
% s3-credentials list-roles AWSServiceRoleForLightsail --details
[
{
"Path": "/aws-service-role/lightsail.amazonaws.com/",
"RoleName": "AWSServiceRoleForLightsail",
"RoleId": "AROAWXFXAIOZG5ACQ5NZ5",
"Arn": "arn:aws:iam::462092780466:role/aws-service-role/lightsail.amazonaws.com/AWSServiceRoleForLightsail",
"CreateDate": "2021-01-15 21:41:48+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lightsail.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"MaxSessionDuration": 3600,
"inline_policies": [
{
"RoleName": "AWSServiceRoleForLightsail",
"PolicyName": "LightsailExportAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:DescribeKey",
"kms:CreateGrant"
],
"Resource": "arn:aws:kms:*:451833091580:key/*"
},
{
"Effect": "Allow",
"Action": [
"cloudformation:DescribeStacks"
],
"Resource": "arn:aws:cloudformation:*:*:stack/*/*"
}
]
}
}
],
"attached_policies": [
{
"PolicyName": "LightsailExportAccess",
"PolicyId": "ANPAJ4LZGPQLZWMVR4WMQ",
"Arn": "arn:aws:iam::aws:policy/aws-service-role/LightsailExportAccess",
"Path": "/aws-service-role/",
"DefaultVersionId": "v2",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "AWS Lightsail service linked role policy which grants permissions to export resources",
"CreateDate": "2018-09-28 16:35:54+00:00",
"UpdateDate": "2022-01-15 01:45:33+00:00",
"Tags": [],
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:DeleteServiceLinkedRole",
"iam:GetServiceLinkedRoleDeletionStatus"
],
"Resource": "arn:aws:iam::*:role/aws-service-role/lightsail.amazonaws.com/AWSServiceRoleForLightsail*"
},
{
"Effect": "Allow",
"Action": [
"ec2:CopySnapshot",
"ec2:DescribeSnapshots",
"ec2:CopyImage",
"ec2:DescribeImages"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetAccountPublicAccessBlock"
],
"Resource": "*"
}
]
},
"VersionId": "v2",
"IsDefaultVersion": true,
"CreateDate": "2022-01-15 01:45:33+00:00"
}
}
]
}
]
```
Add `--nl` to collapse these to single lines as valid newline-delimited JSON.

Add `--csv` or `--tsv` to get back CSV or TSV data.

### delete-user

In trying out this tool it's possible you will create several different user accounts that you later decide to clean up.
Expand Down
88 changes: 87 additions & 1 deletion s3_credentials/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,77 @@ def list_users(nl, csv, tsv, **boto_options):
)


@cli.command()
@click.argument("role_names", nargs=-1)
@click.option("--details", help="Include attached policies (slower)", is_flag=True)
@common_output_options
@common_boto3_options
def list_roles(role_names, details, nl, csv, tsv, **boto_options):
"List all roles"
iam = make_client("iam", **boto_options)
headers = (
"Path",
"RoleName",
"RoleId",
"Arn",
"CreateDate",
"AssumeRolePolicyDocument",
"Description",
"MaxSessionDuration",
"PermissionsBoundary",
"Tags",
"RoleLastUsed",
)
if details:
headers += ("inline_policies", "attached_policies")

def iterate():
for role in paginate(iam, "list_roles", "Roles"):
if role_names and role["RoleName"] not in role_names:
continue
if details:
role_name = role["RoleName"]
role["inline_policies"] = []
# Get inline policy names, then policy for each one
for policy_name in paginate(
iam, "list_role_policies", "PolicyNames", RoleName=role_name
):
role_policy_response = iam.get_role_policy(
RoleName=role_name,
PolicyName=policy_name,
)
role_policy_response.pop("ResponseMetadata", None)
role["inline_policies"].append(role_policy_response)

# Get attached managed policies
role["attached_policies"] = []
for attached in paginate(
iam,
"list_attached_role_policies",
"AttachedPolicies",
RoleName=role_name,
):
policy_arn = attached["PolicyArn"]
attached_policy_response = iam.get_policy(
PolicyArn=policy_arn,
)
policy_details = attached_policy_response["Policy"]
# Also need to fetch the policy JSON
version_id = policy_details["DefaultVersionId"]
policy_version_response = iam.get_policy_version(
PolicyArn=policy_arn,
VersionId=version_id,
)
policy_details["PolicyVersion"] = policy_version_response[
"PolicyVersion"
]
role["attached_policies"].append(policy_details)

yield role

output(iterate(), headers, nl, csv, tsv)


@cli.command()
@click.argument("usernames", nargs=-1)
@common_boto3_options
Expand Down Expand Up @@ -782,7 +853,7 @@ def output(iterator, headers, nl, csv, tsv):
sys.stdout, headers, dialect="excel-tab" if tsv else "excel"
)
writer.writeheader()
writer.writerows(iterator)
writer.writerows(fix_json(row) for row in iterator)
else:
for line in stream_indented_json(iterator):
click.echo(line)
Expand Down Expand Up @@ -817,3 +888,18 @@ def paginate(service, method, list_key, **kwargs):
paginator = service.get_paginator(method)
for response in paginator.paginate(**kwargs):
yield from response[list_key]


def fix_json(row):
# If a key value is list or dict, json encode it
return dict(
[
(
key,
json.dumps(value, indent=2, default=str)
if isinstance(value, (dict, list, tuple))
else value,
)
for key, value in row.items()
]
)
98 changes: 98 additions & 0 deletions tests/test_s3_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,3 +805,101 @@ def test_list_bucket(stub_s3, options, expected):
result = runner.invoke(cli, ["list-bucket", "test-bucket"] + options)
assert result.exit_code == 0
assert result.output == expected


@pytest.fixture
def stub_iam_for_list_roles(stub_iam):
stub_iam.add_response(
"list_roles",
{
"Roles": [
{
"RoleName": "role-one",
"Path": "/",
"Arn": "arn:aws:iam::462092780466:role/role-one",
"RoleId": "36b2eeee501c5952a8ac119f9e521",
"CreateDate": "2020-01-01 00:00:00+00:00",
}
]
},
)
stub_iam.add_response(
"list_role_policies",
{"PolicyNames": ["policy-one"]},
)
stub_iam.add_response(
"get_role_policy",
{
"RoleName": "role-one",
"PolicyName": "policy-one",
"PolicyDocument": '{"foo": "bar}',
},
)
stub_iam.add_response(
"list_attached_role_policies",
{"AttachedPolicies": [{"PolicyArn": "arn:123:must-be-at-least-tweny-chars"}]},
)
stub_iam.add_response(
"get_policy",
{"Policy": {"DefaultVersionId": "v1"}},
)
stub_iam.add_response(
"get_policy_version",
{"PolicyVersion": {"CreateDate": "2020-01-01 00:00:00+00:00"}},
)


@pytest.mark.parametrize("details", (False, True))
def test_list_roles_details(stub_iam_for_list_roles, details):
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli, ["list-roles"] + (["--details"] if details else []))
assert result.exit_code == 0
expected = {
"RoleName": "role-one",
"Path": "/",
"Arn": "arn:aws:iam::462092780466:role/role-one",
"RoleId": "36b2eeee501c5952a8ac119f9e521",
"CreateDate": "2020-01-01 00:00:00+00:00",
"inline_policies": [
{
"RoleName": "role-one",
"PolicyName": "policy-one",
"PolicyDocument": '{"foo": "bar}',
}
],
"attached_policies": [
{
"DefaultVersionId": "v1",
"PolicyVersion": {"CreateDate": "2020-01-01 00:00:00+00:00"},
}
],
}
if not details:
expected.pop("inline_policies")
expected.pop("attached_policies")
assert json.loads(result.output) == [expected]


def test_list_roles_csv(stub_iam_for_list_roles):
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli, ["list-roles", "--csv", "--details"])
assert result.exit_code == 0
assert result.output == (
"Path,RoleName,RoleId,Arn,CreateDate,AssumeRolePolicyDocument,Description,MaxSessionDuration,PermissionsBoundary,Tags,RoleLastUsed,inline_policies,attached_policies\n"
'/,role-one,36b2eeee501c5952a8ac119f9e521,arn:aws:iam::462092780466:role/role-one,2020-01-01 00:00:00+00:00,,,,,,,"[\n'
" {\n"
' ""RoleName"": ""role-one"",\n'
' ""PolicyName"": ""policy-one"",\n'
' ""PolicyDocument"": ""{\\""foo\\"": \\""bar}""\n'
" }\n"
']","[\n'
" {\n"
' ""DefaultVersionId"": ""v1"",\n'
' ""PolicyVersion"": {\n'
' ""CreateDate"": ""2020-01-01 00:00:00+00:00""\n'
" }\n"
" }\n"
']"\n'
)

0 comments on commit 7fb4db1

Please sign in to comment.