From 41bcc8c5100dc508de60f1f5af46551c60d92a93 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 3 Nov 2021 12:00:39 -0700 Subject: [PATCH] Options --access-key, --secret-key, --session-token, --endpoint-url Closes #2 --- README.md | 59 +++++++++++++++++++++------------- s3_credentials/cli.py | 73 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 98 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index a773ed0..35cee0d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,13 @@ Install this tool using `pip`: ## Configuration -This tool uses [boto3](https://boto3.amazonaws.com/) under the hood which supports [a number of different ways](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) of providing your AWS credentials. If you have an existing `~/.aws/config` or `~/.aws/credentials` file the tool will use that - otherwise you can set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables before calling this tool. +This tool uses [boto3](https://boto3.amazonaws.com/) under the hood which supports [a number of different ways](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) of providing your AWS credentials. + +If you have an existing `~/.aws/config` or `~/.aws/credentials` file the tool will use that. + +You can set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables before calling this tool. + +You can also use the `--access-key=` and `--secret-key=` options documented below. ## Usage @@ -151,27 +157,27 @@ To see a list of inline policies belonging to users: User: s3.read-write.static.niche-museums.com PolicyName: s3.read-write.static.niche-museums.com { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "ListObjectsInBucket", - "Effect": "Allow", - "Action": [ - "s3:ListBucket" - ], - "Resource": [ - "arn:aws:s3:::static.niche-museums.com" - ] - }, - { - "Sid": "AllObjectActions", - "Effect": "Allow", - "Action": "s3:*Object", - "Resource": [ - "arn:aws:s3:::static.niche-museums.com/*" - ] - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ListObjectsInBucket", + "Effect": "Allow", + "Action": [ + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::static.niche-museums.com" + ] + }, + { + "Sid": "AllObjectActions", + "Effect": "Allow", + "Action": "s3:*Object", + "Resource": [ + "arn:aws:s3:::static.niche-museums.com/*" + ] + } + ] } ``` You can pass any number of usernames here. If you don't specify a username the tool will loop through every user belonging to your account: @@ -195,6 +201,15 @@ User: s3.read-write.simonw-test-bucket-10 ``` You can pass it multiple usernames to delete multiple users at a time. +## Common options + +All of the `s3-credentials` commands also accept the following options for authenticating against AWS: + +- `--access-key`: AWS access key ID +- `--secret-key`: AWS secret access key +- `--session-token`: AWS session token +- `--endpoint-url`: Custom endpoint URL + ## 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 8970b36..f797238 100644 --- a/s3_credentials/cli.py +++ b/s3_credentials/cli.py @@ -1,3 +1,4 @@ +from re import A import boto3 import botocore import click @@ -21,6 +22,31 @@ def user_exists(iam, username): return False +def common_boto3_options(fn): + for decorator in reversed( + ( + click.option( + "--access-key", + help="AWS access key ID", + ), + click.option( + "--secret-key", + help="AWS secret access key", + ), + click.option( + "--session-token", + help="AWS session token", + ), + click.option( + "--endpoint-url", + help="Custom endpoint URL", + ), + ) + ): + fn = decorator(fn) + return fn + + @click.group() @click.version_option() def cli(): @@ -87,6 +113,7 @@ class PolicyParam(click.ParamType): "--read-only and --write-only options." ), ) +@common_boto3_options def create( buckets, username, @@ -97,6 +124,10 @@ def create( bucket_region, user_permissions_boundary, silent, + access_key, + secret_key, + session_token, + endpoint_url, ): "Create and return new AWS credentials for specified S3 buckets" if read_only and write_only: @@ -113,8 +144,8 @@ def create( permission = "read-only" if write_only: permission = "write-only" - s3 = boto3.client("s3") - iam = boto3.client("iam") + s3 = make_client("s3", access_key, secret_key, session_token, endpoint_url) + iam = make_client("iam", access_key, secret_key, session_token, endpoint_url) # Verify buckets for bucket in buckets: # Create bucket if it doesn't exist @@ -201,18 +232,20 @@ def create( @cli.command() -def whoami(): +@common_boto3_options +def whoami(access_key, secret_key, session_token, endpoint_url): "Identify currently authenticated user" - iam = boto3.client("iam") + iam = make_client("iam", access_key, secret_key, session_token, endpoint_url) click.echo(json.dumps(iam.get_user()["User"], indent=4, default=str)) @cli.command() @click.option("--array", help="Output a valid JSON array", is_flag=True) @click.option("--nl", help="Output newline-delimited JSON", is_flag=True) -def list_users(array, nl): +@common_boto3_options +def list_users(array, nl, access_key, secret_key, session_token, endpoint_url): "List all users" - iam = boto3.client("iam") + iam = make_client("iam", access_key, secret_key, session_token, endpoint_url) paginator = iam.get_paginator("list_users") gathered = [] for response in paginator.paginate(): @@ -230,9 +263,10 @@ def list_users(array, nl): @cli.command() @click.argument("usernames", nargs=-1) -def list_user_policies(usernames): +@common_boto3_options +def list_user_policies(usernames, access_key, secret_key, session_token, endpoint_url): "List inline policies for specified user" - iam = boto3.client("iam") + iam = make_client("iam", access_key, secret_key, session_token, endpoint_url) if not usernames: usernames = [] paginator = iam.get_paginator("list_users") @@ -257,9 +291,10 @@ def list_user_policies(usernames): @cli.command() @click.option("--array", help="Output a valid JSON array", is_flag=True) @click.option("--nl", help="Output newline-delimited JSON", is_flag=True) -def list_buckets(array, nl): +@common_boto3_options +def list_buckets(array, nl, access_key, secret_key, session_token, endpoint_url): "List all buckets" - s3 = boto3.client("s3") + s3 = make_client("s3", access_key, secret_key, session_token, endpoint_url) gathered = [] for bucket in s3.list_buckets()["Buckets"]: if array: @@ -275,9 +310,10 @@ def list_buckets(array, nl): @cli.command() @click.argument("usernames", nargs=-1, required=True) -def delete_user(usernames): +@common_boto3_options +def delete_user(usernames, access_key, secret_key, session_token, endpoint_url): "Delete specified users, their access keys and their inline policies" - iam = boto3.client("iam") + iam = make_client("iam", access_key, secret_key, session_token, endpoint_url) policy_paginator = iam.get_paginator("list_user_policies") access_key_paginator = iam.get_paginator("list_access_keys") for username in usernames: @@ -306,3 +342,16 @@ def delete_user(usernames): click.echo(" Deleted access key: {}".format(access_key_id)) iam.delete_user(UserName=username) click.echo(" Deleted user") + + +def make_client(service, access_key, secret_key, session_token, endpoint_url): + kwargs = {} + if access_key: + kwargs["aws_access_key_id"] = access_key + if secret_key: + kwargs["aws_secret_access_key"] = secret_key + if session_token: + kwargs["aws_session_token"] = session_token + if endpoint_url: + kwargs["endpoint_url"] = endpoint_url + return boto3.client(service, **kwargs)