Skip to content

Commit 94ce3f6

Browse files
FiveSlashNineGeorge Zefi
andauthored
Tighten file permissions when writing credentials in CodeArtifact login.py (#10192)
Co-authored-by: George Zefi <kzefi@amazon.com>
1 parent 7dd6cb5 commit 94ce3f6

3 files changed

Lines changed: 99 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
@@ -191,7 +191,17 @@ def _update_netrc_entry(self, hostname, new_entry, netrc_path):
191191
new_contents, new_entry
192192
)
193193

194-
with open(netrc_path, 'w') as f:
194+
fd = os.open(netrc_path,
195+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
196+
197+
try:
198+
os.chmod(netrc_path, 0o600) # Ensure secure perms on pre-existing files
199+
except OSError as e:
200+
uni_print('Unable to set file permissions '
201+
'for %s: %s%s' % (netrc_path, e, os.linesep),
202+
sys.stderr)
203+
204+
with os.fdopen(fd, 'w') as f:
195205
f.write(new_contents)
196206

197207
def _create_netrc_file(self, netrc_path, new_entry):
@@ -635,7 +645,17 @@ def login(self, dry_run=False):
635645
sys.stdout.write(pypi_rc_str)
636646
sys.stdout.write(os.linesep)
637647
else:
638-
with open(self.pypi_rc_path, 'w+') as fp:
648+
fd = os.open(self.pypi_rc_path,
649+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
650+
651+
try:
652+
os.chmod(self.pypi_rc_path, 0o600) # Ensure secure perms on pre-existing files
653+
except OSError as e:
654+
uni_print('Unable to set file permissions '
655+
'for %s: %s%s' % (self.pypi_rc_path, e, os.linesep),
656+
sys.stderr)
657+
658+
with os.fdopen(fd, 'w') as fp:
639659
fp.write(pypi_rc_str)
640660

641661
self._write_success_message('twine')

tests/unit/customizations/codeartifact/test_adapter_login.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,39 @@ def test_login_dry_run(self):
425425
self.subprocess_utils.check_output.assert_not_called()
426426
self.assertFalse(os.path.exists(self.test_netrc_path))
427427

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

429462
class TestNuGetLogin(unittest.TestCase):
430463
_NUGET_INDEX_URL_FMT = NuGetLogin._NUGET_INDEX_URL_FMT
@@ -1316,6 +1349,45 @@ def test_login_existing_pypi_rc_with_codeartifact_not_clobbered(self):
13161349
password='JgCXIr5xGG',
13171350
)
13181351

1352+
@skip_if_windows("Unix file permissions are not supported on Windows.")
1353+
def test_login_sets_secure_permissions_on_new_file(self):
1354+
self.assertFalse(os.path.exists(self.test_pypi_rc_path))
1355+
self.test_subject.login()
1356+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1357+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1358+
1359+
@skip_if_windows("Unix file permissions are not supported on Windows.")
1360+
def test_login_adjusts_permissions_on_preexisting_file(self):
1361+
existing_pypi_rc = '''\
1362+
[distutils]
1363+
index-servers=
1364+
pypi
1365+
1366+
[pypi]
1367+
repository: http://www.python.org/pypi/
1368+
username: test
1369+
password: JgCXIr5xGG
1370+
'''
1371+
with open(self.test_pypi_rc_path, 'w') as f:
1372+
f.write(existing_pypi_rc)
1373+
os.chmod(self.test_pypi_rc_path, 0o644)
1374+
self.test_subject.login()
1375+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1376+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1377+
1378+
@skip_if_windows("Unix file permissions are not supported on Windows.")
1379+
@mock.patch('sys.stderr')
1380+
@mock.patch('os.chmod', side_effect=OSError(
1381+
errno.EPERM, 'Operation not permitted'
1382+
))
1383+
def test_login_writes_error_when_chmod_fails(self, mock_chmod, mock_stderr):
1384+
self.test_subject.login()
1385+
mock_stderr.write.assert_any_call(
1386+
'Unable to set file permissions for %s: '
1387+
'[Errno 1] Operation not permitted%s'
1388+
% (self.test_pypi_rc_path, os.linesep)
1389+
)
1390+
13191391
def test_login_existing_invalid_pypi_rc_error(self):
13201392
# This is an invalid pypirc as the list of servers are expected under
13211393
# an 'index-servers' option instead of 'servers'.

0 commit comments

Comments
 (0)