Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-s3streamingoutput-59585.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "bugfix",
"category": "s3, streaming output",
"description": "Output files created by S3 Select and streaming output commands are now created with owner-only permissions (0600). Existing files are also tightened to 0600 when overwritten."
}
13 changes: 9 additions & 4 deletions awscli/customizations/s3events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# language governing permissions and limitations under the License.
"""Add S3 specific event streaming output arg."""

import os

from awscli.arguments import CustomArgument

STREAM_HELP_TEXT = 'Filename where the records will be saved'
Expand Down Expand Up @@ -59,8 +61,7 @@ def replace_event_stream_docs(help_command, **kwargs):
# This should never happen, but in the rare case that it does
# we should be raising something with a helpful error message.
raise DocSectionNotFoundError(
'Could not find the "output" section for the command: %s'
% help_command
f'Could not find the "output" section for the command: {help_command}'
)
doc.write('======\nOutput\n======\n')
doc.write(
Expand Down Expand Up @@ -98,7 +99,7 @@ class S3SelectStreamOutputArgument(CustomArgument):
_DOCUMENT_AS_REQUIRED = True

def __init__(self, stream_key, session, **kwargs):
super(S3SelectStreamOutputArgument, self).__init__(**kwargs)
super().__init__(**kwargs)
# This is the key in the response body where we can find the
# streamed contents.
self._stream_key = stream_key
Expand All @@ -120,7 +121,11 @@ def save_file(self, parsed, **kwargs):
if self._stream_key not in parsed:
return
event_stream = parsed[self._stream_key]
with open(self._output_file, 'wb') as fp:
fd = os.open(
self._output_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600
)
os.chmod(self._output_file, 0o600)
Comment thread
kdaily marked this conversation as resolved.
with os.fdopen(fd, 'wb') as fp:
for event in event_stream:
if 'Records' in event:
fp.write(event['Records']['Payload'])
Expand Down
10 changes: 8 additions & 2 deletions awscli/customizations/streamingoutputarg.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os

from botocore.model import Shape

from awscli.arguments import BaseCLIArgument
Expand Down Expand Up @@ -92,7 +94,7 @@ def add_to_params(self, parameters, value):
service_id = self._operation_model.service_model.service_id.hyphenize()
operation_name = self._operation_model.name
self._session.register(
'after-call.%s.%s' % (service_id, operation_name), self.save_file
f'after-call.{service_id}.{operation_name}', self.save_file
)

def save_file(self, parsed, **kwargs):
Expand All @@ -104,7 +106,11 @@ def save_file(self, parsed, **kwargs):
return
body = parsed[self._response_key]
buffer_size = self._buffer_size
with open(self._output_file, 'wb') as fp:
fd = os.open(
self._output_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600
)
os.chmod(self._output_file, 0o600)
with os.fdopen(fd, 'wb') as fp:
data = body.read(buffer_size)
while data:
fp.write(data)
Expand Down
31 changes: 28 additions & 3 deletions tests/functional/s3api/test_select_object_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@
import shutil
import tempfile

from awscli.testutils import BaseAWSCommandParamsTest, BaseAWSHelpOutputTest
from awscli.testutils import (
BaseAWSCommandParamsTest,
BaseAWSHelpOutputTest,
skip_if_windows,
)


class TestGetObject(BaseAWSCommandParamsTest):
prefix = ['s3api', 'select-object-content']

def setUp(self):
super(TestGetObject, self).setUp()
super().setUp()
self.parsed_response = {'Payload': self.create_fake_payload()}
self._tempdir = tempfile.mkdtemp()

def tearDown(self):
super(TestGetObject, self).tearDown()
super().tearDown()
shutil.rmtree(self._tempdir)

def create_fake_payload(self):
Expand Down Expand Up @@ -82,6 +86,27 @@ def test_can_stream_to_file(self):
contents = f.read()
self.assertEqual(contents, ('a,b,c,d\n' 'e,f,g,h\n'))

@skip_if_windows('chmod is not supported on Windows')
def test_output_file_permissions(self):
filename = os.path.join(self._tempdir, 'outfile_perms')
cmdline = self.prefix + [
'--bucket',
'mybucket',
'--key',
'mykey',
'--expression',
'SELECT * FROM S3Object',
'--expression-type',
'SQL',
'--input-serialization',
'{"CSV": {}}',
'--output-serialization',
'{"CSV": {}}',
filename,
]
self.assert_params_for_cmd(cmdline, ignore_params=True)
self.assertEqual(os.stat(filename).st_mode & 0o777, 0o600)
Comment thread
kdaily marked this conversation as resolved.

def test_errors_are_propagated(self):
self.http_response.status_code = 400
self.parsed_response = {
Expand Down
26 changes: 23 additions & 3 deletions tests/functional/test_streaming_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os

from awscli.compat import BytesIO
from awscli.testutils import BaseAWSCommandParamsTest, FileCreator
from awscli.testutils import (
BaseAWSCommandParamsTest,
FileCreator,
skip_if_windows,
)


class TestStreamingOutput(BaseAWSCommandParamsTest):
def setUp(self):
super(TestStreamingOutput, self).setUp()
super().setUp()
self.files = FileCreator()

def tearDown(self):
super(TestStreamingOutput, self).tearDown()
super().tearDown()
self.files.remove_all()

def test_get_media_streaming_output(self):
Expand All @@ -41,3 +47,17 @@ def test_get_media_streaming_output(self):
self.assert_params_for_cmd(cmdline % outpath, params)
with open(outpath, 'rb') as outfile:
self.assertEqual(outfile.read(), b'testbody')

@skip_if_windows('chmod is not supported on Windows')
def test_streaming_output_file_permissions(self):
cmdline = (
'kinesis-video-media get-media --stream-name test-stream '
'--start-selector StartSelectorType=EARLIEST %s'
)
self.parsed_response = {
'ContentType': 'video/webm',
'Payload': BytesIO(b'testbody'),
}
outpath = self.files.full_path('outfile')
self.assert_params_for_cmd(cmdline % outpath, ignore_params=True)
self.assertEqual(os.stat(outpath).st_mode & 0o777, 0o600)
Loading