--public option for creating public buckets, closes #42

Will help with buckets as websites in #21

Includes integration test cowerage for put-object content-type in #43
pull/45/head
Simon Willison 2021-12-06 22:54:47 -08:00
rodzic a2a642d616
commit 6e0a2c2029
4 zmienionych plików z 106 dodań i 0 usunięć

Wyświetl plik

@ -90,6 +90,7 @@ The `create` command has a number of options:
- `--duration 15m`: For temporary credentials, how long should they last? This can be specified in seconds, minutes or hours using a suffix of `s`, `m` or `h` - but must be between 15 minutes and 12 hours.
- `--username TEXT`: The username to use for the user that is created by the command (or the username of an existing user if you do not want to create a new one). If ommitted a default such as `s3.read-write.static.niche-museums.com` will be used.
- `-c, --create-bucket`: Create the buckets if they do not exist. Without this any missing buckets will be treated as an error.
- `--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.
- `--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.

Wyświetl plik

@ -186,6 +186,11 @@ def policy(buckets, read_only, write_only, public_bucket):
help="Create buckets if they do not already exist",
is_flag=True,
)
@click.option(
"--public",
help="Make the created bucket public: anyone will be able to download files if they know their name",
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(
@ -211,6 +216,7 @@ def create(
duration,
username,
create_bucket,
public,
read_only,
write_only,
policy,
@ -263,6 +269,10 @@ def create(
"LocationConstraint": bucket_region
}
}
bucket_policy = {}
if public:
bucket_policy = policies.bucket_policy_allow_all_get(bucket)
if dry_run:
click.echo(
"Would create bucket: '{}'{}".format(
@ -274,12 +284,20 @@ def create(
),
)
)
if bucket_policy:
click.echo("... then the following bucket policy:")
click.echo(json.dumps(bucket_policy, indent=4))
else:
s3.create_bucket(Bucket=bucket, **kwargs)
info = "Created bucket: {}".format(bucket)
if bucket_region:
info += "in region: {}".format(bucket_region)
log(info)
if bucket_policy:
s3.put_bucket_policy(
Bucket=bucket, Policy=json.dumps(bucket_policy)
)
log("Attached bucket policy allowing public access")
# At this point the buckets definitely exist - create the inline policy
bucket_access_policy = {}
if policy:

Wyświetl plik

@ -10,6 +10,7 @@ import json
import pytest
import secrets
import time
import urllib
# Mark all tests in this module with "integration":
pytestmark = pytest.mark.integration
@ -131,3 +132,45 @@ def cleanup_any_resources():
boto3.resource("s3").Bucket(bucket).objects.all().delete()
# Delete the bucket
s3.delete_bucket(Bucket=bucket)
def test_public_bucket():
bucket_name = "s3-credentials-tests.public-bucket.{}".format(secrets.token_hex(4))
s3 = boto3.client("s3")
assert not bucket_exists(s3, bucket_name)
credentials_decoded = json.loads(
get_output("create", bucket_name, "-c", "--duration", "15m", "--public")
)
assert set(credentials_decoded.keys()) == {
"AccessKeyId",
"SecretAccessKey",
"SessionToken",
"Expiration",
}
# Wait for everything to exist
time.sleep(5)
# Use those credentials to upload a file
content = "<h1>Hello world</h1>"
get_output(
"put-object",
bucket_name,
"hello.html",
"-",
"--content-type",
"text/html",
"--access-key",
credentials_decoded["AccessKeyId"],
"--secret-key",
credentials_decoded["SecretAccessKey"],
"--session-token",
credentials_decoded["SessionToken"],
input=content,
)
# It should be publicly accessible
url = "https://s3.amazonaws.com/{}/hello.html".format(bucket_name)
print(url)
response = urllib.request.urlopen(url)
actual_content = response.read().decode("utf-8")
assert response.status == 200
assert response.headers["content-type"] == "text/html"
assert actual_content == content

Wyświetl plik

@ -432,6 +432,50 @@ def test_create_duration(
]
def test_create_public(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", "--public"]
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"
"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().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()