Skip to content

Commit e0799fd

Browse files
FiveSlashNineGeorge Zefi
andauthored
Tighten file permissions when writing credentials in CodeArtifact login.py (#10191)
Co-authored-by: George Zefi <kzefi@amazon.com>
1 parent 6c0f166 commit e0799fd

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "CodeArtifact",
4+
"description": "Tighten file permissions when writing credentials in CodeArtifact login"
5+
}

awscli/customizations/codeartifact/login.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,17 @@ def _update_netrc_entry(self, hostname, new_entry, netrc_path):
183183
if new_contents == contents:
184184
new_contents = self._append_netrc_entry(new_contents, new_entry)
185185

186-
with open(netrc_path, 'w') as f:
186+
fd = os.open(netrc_path,
187+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
188+
189+
try:
190+
os.chmod(netrc_path, 0o600) # Ensure secure perms on pre-existing files
191+
except OSError as e:
192+
uni_print('Unable to set file permissions '
193+
'for %s: %s%s' % (netrc_path, e, os.linesep),
194+
sys.stderr)
195+
196+
with os.fdopen(fd, 'w') as f:
187197
f.write(new_contents)
188198

189199
def _create_netrc_file(self, netrc_path, new_entry):
@@ -631,7 +641,17 @@ def login(self, dry_run=False):
631641
sys.stdout.write(pypi_rc_str)
632642
sys.stdout.write(os.linesep)
633643
else:
634-
with open(self.pypi_rc_path, 'w+') as fp:
644+
fd = os.open(self.pypi_rc_path,
645+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
646+
647+
try:
648+
os.chmod(self.pypi_rc_path, 0o600) # Ensure secure perms on pre-existing files
649+
except OSError as e:
650+
uni_print('Unable to set file permissions '
651+
'for %s: %s%s' % (self.pypi_rc_path, e, os.linesep),
652+
sys.stderr)
653+
654+
with os.fdopen(fd, 'w') as fp:
635655
fp.write(pypi_rc_str)
636656

637657
self._write_success_message('twine')

tests/unit/customizations/codeartifact/test_adapter_login.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,40 @@ def test_login_dry_run(self):
435435
self.subprocess_utils.check_output.assert_not_called()
436436
self.assertFalse(os.path.exists(self.test_netrc_path))
437437

438+
@skip_if_windows("Unix file permissions are not supported on Windows.")
439+
@mock.patch('awscli.customizations.codeartifact.login.is_macos', False)
440+
def test_login_adjusts_permissions_on_preexisting_file(self):
441+
existing_content = (
442+
f'machine {self.hostname} login token password old-token\n'
443+
)
444+
with open(self.test_netrc_path, 'w') as f:
445+
f.write(existing_content)
446+
os.chmod(self.test_netrc_path, 0o644)
447+
self.test_subject.login()
448+
file_mode = os.stat(self.test_netrc_path).st_mode
449+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
450+
451+
@skip_if_windows("Unix file permissions are not supported on Windows.")
452+
@mock.patch('awscli.customizations.codeartifact.login.is_macos', False)
453+
@mock.patch('sys.stderr')
454+
@mock.patch('os.chmod', side_effect=OSError(
455+
errno.EPERM, 'Operation not permitted'
456+
))
457+
def test_login_writes_error_when_chmod_fails(
458+
self, mock_chmod, mock_stderr
459+
):
460+
existing_content = (
461+
f'machine {self.hostname} login token password old-token\n'
462+
)
463+
with open(self.test_netrc_path, 'w') as f:
464+
f.write(existing_content)
465+
self.test_subject.login()
466+
mock_stderr.write.assert_any_call(
467+
'Unable to set file permissions for %s: '
468+
'[Errno 1] Operation not permitted%s'
469+
% (self.test_netrc_path, os.linesep)
470+
)
471+
438472

439473
class TestNuGetLogin(unittest.TestCase):
440474
_NUGET_INDEX_URL_FMT = NuGetLogin._NUGET_INDEX_URL_FMT
@@ -1246,6 +1280,45 @@ def test_login_existing_pypi_rc_with_codeartifact_not_clobbered(self):
12461280
password='JgCXIr5xGG'
12471281
)
12481282

1283+
@skip_if_windows("Unix file permissions are not supported on Windows.")
1284+
def test_login_sets_secure_permissions_on_new_file(self):
1285+
self.assertFalse(os.path.exists(self.test_pypi_rc_path))
1286+
self.test_subject.login()
1287+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1288+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1289+
1290+
@skip_if_windows("Unix file permissions are not supported on Windows.")
1291+
def test_login_adjusts_permissions_on_preexisting_file(self):
1292+
existing_pypi_rc = '''\
1293+
[distutils]
1294+
index-servers=
1295+
pypi
1296+
1297+
[pypi]
1298+
repository: http://www.python.org/pypi/
1299+
username: test
1300+
password: JgCXIr5xGG
1301+
'''
1302+
with open(self.test_pypi_rc_path, 'w') as f:
1303+
f.write(existing_pypi_rc)
1304+
os.chmod(self.test_pypi_rc_path, 0o644)
1305+
self.test_subject.login()
1306+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1307+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1308+
1309+
@skip_if_windows("Unix file permissions are not supported on Windows.")
1310+
@mock.patch('sys.stderr')
1311+
@mock.patch('os.chmod', side_effect=OSError(
1312+
errno.EPERM, 'Operation not permitted'
1313+
))
1314+
def test_login_writes_error_when_chmod_fails(self, mock_chmod, mock_stderr):
1315+
self.test_subject.login()
1316+
mock_stderr.write.assert_any_call(
1317+
'Unable to set file permissions for %s: '
1318+
'[Errno 1] Operation not permitted%s'
1319+
% (self.test_pypi_rc_path, os.linesep)
1320+
)
1321+
12491322
def test_login_existing_invalid_pypi_rc_error(self):
12501323
# This is an invalid pypirc as the list of servers are expected under
12511324
# an 'index-servers' option instead of 'servers'.

0 commit comments

Comments
 (0)