Skip to content

Commit 95853a2

Browse files
Tighten history file permissions (aws#10028)
* Tighten history file permissions * Pre-commit formatting * add changelog entry * Update file permissions fix * Show warning instead of failing when history database cannot be opened
1 parent 198ea5a commit 95853a2

6 files changed

Lines changed: 487 additions & 283 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "cli-history",
4+
"description": "Create local history files with specific permissions"
5+
}

awscli/customizations/history/__init__.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,49 +10,62 @@
1010
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
13+
import logging
1314
import os
1415
import sys
15-
import logging
1616

17-
from botocore.history import get_global_history_recorder
1817
from botocore.exceptions import ProfileNotFound
18+
from botocore.history import get_global_history_recorder
1919

2020
from awscli.compat import sqlite3
2121
from awscli.customizations.commands import BasicCommand
22-
from awscli.customizations.history.constants import HISTORY_FILENAME_ENV_VAR
23-
from awscli.customizations.history.constants import DEFAULT_HISTORY_FILENAME
24-
from awscli.customizations.history.db import DatabaseConnection
25-
from awscli.customizations.history.db import DatabaseRecordWriter
26-
from awscli.customizations.history.db import RecordBuilder
27-
from awscli.customizations.history.db import DatabaseHistoryHandler
28-
from awscli.customizations.history.show import ShowCommand
22+
from awscli.customizations.history.constants import (
23+
DEFAULT_HISTORY_FILENAME,
24+
HISTORY_FILENAME_ENV_VAR,
25+
)
26+
from awscli.customizations.history.db import (
27+
DatabaseConnection,
28+
DatabaseHistoryHandler,
29+
DatabaseRecordWriter,
30+
RecordBuilder,
31+
)
2932
from awscli.customizations.history.list import ListCommand
30-
33+
from awscli.customizations.history.show import ShowCommand
3134

3235
LOG = logging.getLogger(__name__)
3336
HISTORY_RECORDER = get_global_history_recorder()
3437

3538

3639
def register_history_mode(event_handlers):
37-
event_handlers.register(
38-
'session-initialized', attach_history_handler)
40+
event_handlers.register('session-initialized', attach_history_handler)
3941

4042

4143
def register_history_commands(event_handlers):
4244
event_handlers.register(
43-
"building-command-table.main", add_history_commands)
45+
"building-command-table.main", add_history_commands
46+
)
4447

4548

4649
def attach_history_handler(session, parsed_args, **kwargs):
4750
if _should_enable_cli_history(session, parsed_args):
4851
LOG.debug('Enabling CLI history')
4952

5053
history_filename = os.environ.get(
51-
HISTORY_FILENAME_ENV_VAR, DEFAULT_HISTORY_FILENAME)
52-
if not os.path.isdir(os.path.dirname(history_filename)):
53-
os.makedirs(os.path.dirname(history_filename))
54-
55-
connection = DatabaseConnection(history_filename)
54+
HISTORY_FILENAME_ENV_VAR, DEFAULT_HISTORY_FILENAME
55+
)
56+
history_dir = os.path.dirname(history_filename)
57+
if not os.path.isdir(history_dir):
58+
os.makedirs(history_dir)
59+
60+
try:
61+
connection = DatabaseConnection(history_filename)
62+
except Exception as e:
63+
LOG.debug('Unable to open history database: %s', e)
64+
sys.stderr.write(
65+
'Warning: Unable to record CLI history. '
66+
'Check file permissions for %s\n' % history_filename
67+
)
68+
return
5669
writer = DatabaseRecordWriter(connection)
5770
record_builder = RecordBuilder()
5871
db_handler = DatabaseHistoryHandler(writer, record_builder)
@@ -98,10 +111,12 @@ class HistoryCommand(BasicCommand):
98111
)
99112
SUBCOMMANDS = [
100113
{'name': 'show', 'command_class': ShowCommand},
101-
{'name': 'list', 'command_class': ListCommand}
114+
{'name': 'list', 'command_class': ListCommand},
102115
]
103116

104117
def _run_main(self, parsed_args, parsed_globals):
105118
if parsed_args.subcommand is None:
106-
raise ValueError("usage: aws [options] <command> <subcommand> "
107-
"[parameters]\naws: error: too few arguments")
119+
raise ValueError(
120+
"usage: aws [options] <command> <subcommand> "
121+
"[parameters]\naws: error: too few arguments"
122+
)

awscli/customizations/history/db.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,22 @@
1010
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
13-
import uuid
14-
import time
15-
import json
1613
import datetime
17-
import threading
14+
import json
1815
import logging
19-
from awscli.compat import collections_abc
16+
import os
17+
import threading
18+
import time
19+
import uuid
2020

2121
from botocore.history import BaseHistoryHandler
2222

23-
from awscli.compat import sqlite3
24-
from awscli.compat import binary_type
25-
23+
from awscli.compat import binary_type, collections_abc, sqlite3
2624

2725
LOG = logging.getLogger(__name__)
2826

2927

30-
class DatabaseConnection(object):
28+
class DatabaseConnection:
3129
_CREATE_TABLE = """
3230
CREATE TABLE IF NOT EXISTS records (
3331
id TEXT,
@@ -40,13 +38,26 @@ class DatabaseConnection(object):
4038
_ENABLE_WAL = 'PRAGMA journal_mode=WAL'
4139

4240
def __init__(self, db_filename):
41+
self._db_filename = db_filename
4342
self._connection = sqlite3.connect(
44-
db_filename, check_same_thread=False, isolation_level=None)
43+
db_filename, check_same_thread=False, isolation_level=None
44+
)
45+
self._set_file_permissions()
4546
self._ensure_database_setup()
4647

4748
def close(self):
4849
self._connection.close()
4950

51+
def _set_file_permissions(self):
52+
for suffix in ('', '-wal', '-shm'):
53+
path = self._db_filename + suffix
54+
if not os.path.exists(path):
55+
continue
56+
try:
57+
os.chmod(path, 0o600)
58+
except OSError as e:
59+
LOG.debug('Unable to set file permissions for %s: %s', path, e)
60+
5061
def execute(self, query, *parameters):
5162
return self._connection.execute(query, *parameters)
5263

@@ -92,8 +103,9 @@ def _remove_non_unicode_stings(self, obj):
92103
if isinstance(obj, str):
93104
obj = self._try_decode_bytes(obj)
94105
elif isinstance(obj, dict):
95-
obj = dict((k, self._remove_non_unicode_stings(v)) for k, v
96-
in obj.items())
106+
obj = dict(
107+
(k, self._remove_non_unicode_stings(v)) for k, v in obj.items()
108+
)
97109
elif isinstance(obj, (list, tuple)):
98110
obj = [self._remove_non_unicode_stings(o) for o in obj]
99111
return obj
@@ -132,7 +144,7 @@ def default(self, obj):
132144
return repr(obj)
133145

134146

135-
class DatabaseRecordWriter(object):
147+
class DatabaseRecordWriter:
136148
_WRITE_RECORD = """
137149
INSERT INTO records(
138150
id, request_id, source, event_type, timestamp, payload)
@@ -152,26 +164,30 @@ def write_record(self, record):
152164

153165
def _create_db_record(self, record):
154166
event_type = record['event_type']
155-
json_serialized_payload = json.dumps(record['payload'],
156-
cls=PayloadSerializer)
167+
json_serialized_payload = json.dumps(
168+
record['payload'], cls=PayloadSerializer
169+
)
157170
db_record = (
158171
record['command_id'],
159172
record.get('request_id'),
160173
record['source'],
161174
event_type,
162175
record['timestamp'],
163-
json_serialized_payload
176+
json_serialized_payload,
164177
)
165178
return db_record
166179

167180

168-
class DatabaseRecordReader(object):
181+
class DatabaseRecordReader:
169182
_ORDERING = 'ORDER BY timestamp'
170-
_GET_LAST_ID_RECORDS = """
183+
_GET_LAST_ID_RECORDS = (
184+
"""
171185
SELECT * FROM records
172186
WHERE id =
173187
(SELECT id FROM records WHERE timestamp =
174-
(SELECT max(timestamp) FROM records)) %s;""" % _ORDERING
188+
(SELECT max(timestamp) FROM records)) %s;"""
189+
% _ORDERING
190+
)
175191
_GET_RECORDS_BY_ID = 'SELECT * from records where id = ? %s' % _ORDERING
176192
_GET_ALL_RECORDS = (
177193
'SELECT a.id AS id_a, '
@@ -218,9 +234,10 @@ def iter_all_records(self):
218234
yield row
219235

220236

221-
class RecordBuilder(object):
237+
class RecordBuilder:
222238
_REQUEST_LIFECYCLE_EVENTS = set(
223-
['API_CALL', 'HTTP_REQUEST', 'HTTP_RESPONSE', 'PARSED_RESPONSE'])
239+
['API_CALL', 'HTTP_REQUEST', 'HTTP_RESPONSE', 'PARSED_RESPONSE']
240+
)
224241
_START_OF_REQUEST_LIFECYCLE_EVENT = 'API_CALL'
225242

226243
def __init__(self):
@@ -254,7 +271,7 @@ def build_record(self, event_type, payload, source):
254271
'event_type': event_type,
255272
'payload': payload,
256273
'source': source,
257-
'timestamp': int(time.time() * 1000)
274+
'timestamp': int(time.time() * 1000),
258275
}
259276
request_id = self._get_request_id(event_type)
260277
if request_id:

tests/integration/test_cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,15 @@ def test_version(self):
267267
p.stdout.startswith('aws-cli')
268268
self.assertTrue(version_output, p.stderr)
269269

270+
def test_version_does_not_create_cache_directory(self):
271+
# Regression test: --version should not create any files/directories.
272+
with tempfile.TemporaryDirectory() as tmpdir:
273+
env = os.environ.copy()
274+
env['HOME'] = tmpdir
275+
aws('--version', env_vars=env)
276+
aws_dir = os.path.join(tmpdir, '.aws')
277+
self.assertFalse(os.path.exists(aws_dir))
278+
270279
def test_traceback_printed_when_debug_on(self):
271280
p = aws('ec2 describe-instances --filters BADKEY=foo --debug')
272281
self.assertIn('Traceback (most recent call last):', p.stderr, p.stderr)

0 commit comments

Comments
 (0)