Skip to content

Commit

Permalink
s3-credentials create --website option, closes #21
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Aug 12, 2022
1 parent e9c2fb7 commit 5a9814d
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The `create` command has a number of options:
- `-c, --create-bucket`: Create the buckets if they do not exist. Without this any missing buckets will be treated as an error.
- `--prefix my-prefix/`: Credentials should only allow access to keys in the S3 bucket that start with this prefix.
- `--public`: When creating a bucket, set it so that any file uploaded to that bucket can be downloaded by anyone who knows its filename. This attaches the [public bucket policy](#public-bucket-policy) shown below.
- `--website`: Sets the bucket to public and configures it to act as a website, with `index.html` treated as an index page and `error.html` used to display custom errors. The URL for the website will be `http://<bucket-name>.s3-website.<region>.amazonaws.com/` - the region defaults to `us-east-1` unless you specify a `--bucket-region`.
- `--read-only`: The user should only be allowed to read files from the bucket.
- `--write-only`: The user should only be allowed to write files to the bucket, but not read them. This can be useful for logging and backups.
- `--policy filepath-or-string`: A custom policy document (as a file path, literal JSON string or `-` for standard input) - see below.
Expand Down
25 changes: 25 additions & 0 deletions s3_credentials/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ def policy(buckets, read_only, write_only, prefix, extra_statements, public_buck
help="Make the created bucket public: anyone will be able to download files if they know their name",
is_flag=True,
)
@click.option(
"--website",
help="Configure bucket to act as a website, using index.html and error.html",
is_flag=True,
)
@click.option("--read-only", help="Only allow reading from the bucket", is_flag=True)
@click.option("--write-only", help="Only allow writing to the bucket", is_flag=True)
@click.option(
Expand Down Expand Up @@ -286,6 +291,7 @@ def create(
create_bucket,
prefix,
public,
website,
read_only,
write_only,
policy,
Expand Down Expand Up @@ -331,6 +337,9 @@ def log(message):
if not user_permissions_boundary and (policy or extra_statements):
user_permissions_boundary = "none"

if website:
public = True

s3 = None
iam = None
sts = None
Expand Down Expand Up @@ -376,6 +385,10 @@ def log(message):
if bucket_policy:
click.echo("... then attach the following bucket policy to it:")
click.echo(json.dumps(bucket_policy, indent=4))
if website:
click.echo(
"... then configure index.html and error.html website settings"
)
else:
s3.create_bucket(Bucket=bucket, **kwargs)
info = "Created bucket: {}".format(bucket)
Expand All @@ -388,6 +401,18 @@ def log(message):
Bucket=bucket, Policy=json.dumps(bucket_policy)
)
log("Attached bucket policy allowing public access")
if website:
s3.put_bucket_website(
Bucket=bucket,
WebsiteConfiguration={
"ErrorDocument": {"Key": "error.html"},
"IndexDocument": {"Suffix": "index.html"},
},
)
log(
"Configured website: IndexDocument=index.html, ErrorDocument=error.html"
)

# At this point the buckets definitely exist - create the inline policy for assume_role()
assume_role_policy = {}
if policy:
Expand Down
48 changes: 48 additions & 0 deletions tests/test_s3_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,54 @@ def test_create_public(mocker):
]


def test_create_website(mocker):
boto3 = mocker.patch("boto3.client")
boto3.return_value = Mock()
boto3.return_value.create_access_key.return_value = {
"AccessKey": {"AccessKeyId": "access", "SecretAccessKey": "secret"}
}
# Fake that the bucket does not exist
boto3.return_value.head_bucket.side_effect = botocore.exceptions.ClientError(
error_response={}, operation_name=""
)
runner = CliRunner()
with runner.isolated_filesystem():
args = ["create", "pytest-bucket-simonw-1", "-c", "--website"]
result = runner.invoke(cli, args, catch_exceptions=False)
assert result.exit_code == 0
assert result.output == (
"Created bucket: pytest-bucket-simonw-1\n"
"Attached bucket policy allowing public access\n"
"Configured website: IndexDocument=index.html, ErrorDocument=error.html\n"
"Attached policy s3.read-write.pytest-bucket-simonw-1 to user s3.read-write.pytest-bucket-simonw-1\n"
"Created access key for user: s3.read-write.pytest-bucket-simonw-1\n"
"{\n"
' "AccessKeyId": "access",\n'
' "SecretAccessKey": "secret"\n'
"}\n"
)
assert [str(c) for c in boto3.mock_calls] == [
"call('s3')",
"call('iam')",
"call('sts')",
"call().head_bucket(Bucket='pytest-bucket-simonw-1')",
"call().create_bucket(Bucket='pytest-bucket-simonw-1')",
"call().put_bucket_policy(Bucket='pytest-bucket-simonw-1', "
'Policy=\'{"Version": "2012-10-17", "Statement": [{"Sid": '
'"AllowAllGetObject", "Effect": "Allow", "Principal": "*", "Action": '
'["s3:GetObject"], "Resource": '
'["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}\')',
"call().put_bucket_website(Bucket='pytest-bucket-simonw-1', "
"WebsiteConfiguration={'ErrorDocument': {'Key': 'error.html'}, "
"'IndexDocument': {'Suffix': 'index.html'}})",
"call().get_user(UserName='s3.read-write.pytest-bucket-simonw-1')",
"call().put_user_policy(PolicyDocument='{}', PolicyName='s3.read-write.pytest-bucket-simonw-1', UserName='s3.read-write.pytest-bucket-simonw-1')".format(
READ_WRITE_POLICY.replace("$!BUCKET_NAME!$", "pytest-bucket-simonw-1"),
),
"call().create_access_key(UserName='s3.read-write.pytest-bucket-simonw-1')",
]


def test_create_format_ini(mocker):
boto3 = mocker.patch("boto3.client")
boto3.return_value = Mock()
Expand Down

0 comments on commit 5a9814d

Please sign in to comment.