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.
|
- `--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.
|
- `--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.
|
- `-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.
|
- `--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.
|
- `--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.
|
- `--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",
|
help="Create buckets if they do not already exist",
|
||||||
is_flag=True,
|
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("--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("--write-only", help="Only allow writing to the bucket", is_flag=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
|
@ -211,6 +216,7 @@ def create(
|
||||||
duration,
|
duration,
|
||||||
username,
|
username,
|
||||||
create_bucket,
|
create_bucket,
|
||||||
|
public,
|
||||||
read_only,
|
read_only,
|
||||||
write_only,
|
write_only,
|
||||||
policy,
|
policy,
|
||||||
|
@ -263,6 +269,10 @@ def create(
|
||||||
"LocationConstraint": bucket_region
|
"LocationConstraint": bucket_region
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bucket_policy = {}
|
||||||
|
if public:
|
||||||
|
bucket_policy = policies.bucket_policy_allow_all_get(bucket)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
click.echo(
|
click.echo(
|
||||||
"Would create bucket: '{}'{}".format(
|
"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:
|
else:
|
||||||
s3.create_bucket(Bucket=bucket, **kwargs)
|
s3.create_bucket(Bucket=bucket, **kwargs)
|
||||||
info = "Created bucket: {}".format(bucket)
|
info = "Created bucket: {}".format(bucket)
|
||||||
if bucket_region:
|
if bucket_region:
|
||||||
info += "in region: {}".format(bucket_region)
|
info += "in region: {}".format(bucket_region)
|
||||||
log(info)
|
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
|
# At this point the buckets definitely exist - create the inline policy
|
||||||
bucket_access_policy = {}
|
bucket_access_policy = {}
|
||||||
if policy:
|
if policy:
|
||||||
|
|
|
@ -10,6 +10,7 @@ import json
|
||||||
import pytest
|
import pytest
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
import urllib
|
||||||
|
|
||||||
# Mark all tests in this module with "integration":
|
# Mark all tests in this module with "integration":
|
||||||
pytestmark = pytest.mark.integration
|
pytestmark = pytest.mark.integration
|
||||||
|
@ -131,3 +132,45 @@ def cleanup_any_resources():
|
||||||
boto3.resource("s3").Bucket(bucket).objects.all().delete()
|
boto3.resource("s3").Bucket(bucket).objects.all().delete()
|
||||||
# Delete the bucket
|
# Delete the bucket
|
||||||
s3.delete_bucket(Bucket=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):
|
def test_create_format_ini(mocker):
|
||||||
boto3 = mocker.patch("boto3.client")
|
boto3 = mocker.patch("boto3.client")
|
||||||
boto3.return_value = Mock()
|
boto3.return_value = Mock()
|
||||||
|
|
Ładowanie…
Reference in New Issue