kopia lustrzana https://github.com/simonw/s3-credentials
--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 #43pull/45/head
rodzic
a2a642d616
commit
6e0a2c2029
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
Ładowanie…
Reference in New Issue