Skip to content

Commit c15ea18

Browse files
benridleybenFizzadar
committed
Transfer ownership of files over ssh with sudo/su (#616)
* Transfer ownership of files over ssh with sudo/su. Adds a step to SSH file uploads that assigns read permissions via ACLs to the sudo/su user, and uses cp instead of mv to allow the the intended user to take ownership. * Use format to maintain python2 compatibility * Modify StringCommand for consistent styling. Co-authored-by: Nick Barrett <nick@fizzadar.com> * Modify StringCommand for consistent styling. Co-authored-by: Nick Barrett <nick@fizzadar.com> * Only run setfacl command if a sudo/su user is specified * Remove redundant chown command The chown command is no longer required because we now cp the file as the sudo/su user, so the file is created with the correct ownership. * Remove temporary files after upload * Modify test cases for new SSH upload commands * Linting whitespaces. * Expand SSH connector tests to cover copy & acl failures. Co-authored-by: ben <ben@pop-os.localdomain> Co-authored-by: Nick Barrett <nick@fizzadar.com>
1 parent 28698a1 commit c15ea18

File tree

3 files changed

+76
-15
lines changed

3 files changed

+76
-15
lines changed

pyinfra/api/connectors/ssh.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -420,16 +420,26 @@ def put_file(
420420
temp_file = state.get_temp_filename(remote_filename)
421421
_put_file(host, filename_or_io, temp_file)
422422

423-
# Execute run_shell_command w/sudo and/or su_user
424-
command = StringCommand('mv', temp_file, QuoteString(remote_filename))
425-
426-
# Move it to the su_user if present
423+
# Make sure our sudo/su user can access the file
427424
if su_user:
428-
command = StringCommand(command, '&&', 'chown', su_user, QuoteString(remote_filename))
429-
430-
# Otherwise any sudo_user
425+
command = StringCommand('setfacl', '-m', 'u:{0}:r'.format(su_user), temp_file)
431426
elif sudo_user:
432-
command = StringCommand(command, '&&', 'chown', sudo_user, QuoteString(remote_filename))
427+
command = StringCommand('setfacl -m u:{0}:r'.format(sudo_user), temp_file)
428+
if su_user or sudo_user:
429+
status, _, stderr = run_shell_command(
430+
state, host, command,
431+
sudo=False,
432+
print_output=print_output,
433+
print_input=print_input,
434+
**command_kwargs
435+
)
436+
437+
if status is False:
438+
logger.error('Error on handover to sudo/su user: {0}'.format('\n'.join(stderr)))
439+
return False
440+
441+
# Execute run_shell_command w/sudo and/or su_user
442+
command = StringCommand('cp', temp_file, QuoteString(remote_filename))
433443

434444
status, _, stderr = run_shell_command(
435445
state, host, command,
@@ -443,6 +453,21 @@ def put_file(
443453
logger.error('File upload error: {0}'.format('\n'.join(stderr)))
444454
return False
445455

456+
# Delete the temporary file now that we've successfully copied it
457+
command = StringCommand('rm', '-f', temp_file)
458+
459+
status, _, stderr = run_shell_command(
460+
state, host, command,
461+
sudo=False,
462+
print_output=print_output,
463+
print_input=print_input,
464+
**command_kwargs
465+
)
466+
467+
if status is False:
468+
logger.error('Unable to remove temporary file: {0}'.format('\n'.join(stderr)))
469+
return False
470+
446471
# No sudo and no su_user, so just upload it!
447472
else:
448473
_put_file(host, filename_or_io, remote_filename)

tests/test_connectors/test_ssh.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,8 @@ def test_put_file_sudo(self, fake_sftp_client, fake_ssh_client):
507507
assert status is True
508508

509509
fake_ssh_client().exec_command.assert_called_with((
510-
"sudo -H -n -u ubuntu sh -c 'mv "
511-
'/tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24 '
512-
"\'\"\'\"\'not another file\'\"\'\"\' && chown ubuntu "
513-
"\'\"\'\"\'not another file\'\"\'\"\'\'"
510+
"sh -c 'rm -f "
511+
"/tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24'"
514512
), get_pty=False)
515513

516514
fake_sftp_client.from_transport().putfo.assert_called_with(
@@ -519,7 +517,7 @@ def test_put_file_sudo(self, fake_sftp_client, fake_ssh_client):
519517

520518
@patch('pyinfra.api.connectors.ssh.SSHClient')
521519
@patch('pyinfra.api.connectors.ssh.SFTPClient')
522-
def test_put_file_su_user_fail(self, fake_sftp_client, fake_ssh_client):
520+
def test_put_file_su_user_fail_acl(self, fake_sftp_client, fake_ssh_client):
523521
inventory = make_inventory(hosts=('anotherhost',))
524522
State(inventory, Config())
525523
host = inventory.get_host('anotherhost')
@@ -540,9 +538,46 @@ def test_put_file_su_user_fail(self, fake_sftp_client, fake_ssh_client):
540538
assert status is False
541539

542540
fake_ssh_client().exec_command.assert_called_with((
543-
"su centos -c 'sh -c '\"'\"'mv "
541+
"sh -c 'setfacl -m u:centos:r "
542+
"/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487'"
543+
), get_pty=False)
544+
545+
fake_sftp_client.from_transport().putfo.assert_called_with(
546+
fake_open(), '/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487',
547+
)
548+
549+
@patch('pyinfra.api.connectors.ssh.SSHClient')
550+
@patch('pyinfra.api.connectors.ssh.SFTPClient')
551+
def test_put_file_su_user_fail_copy(self, fake_sftp_client, fake_ssh_client):
552+
inventory = make_inventory(hosts=('anotherhost',))
553+
State(inventory, Config())
554+
host = inventory.get_host('anotherhost')
555+
host.connect()
556+
557+
stdout_mock = MagicMock()
558+
exit_codes = [0, 1]
559+
stdout_mock.channel.recv_exit_status.side_effect = lambda: exit_codes.pop(0)
560+
fake_ssh_client().exec_command.return_value = MagicMock(), stdout_mock, MagicMock()
561+
562+
fake_open = mock_open(read_data='test!')
563+
with patch('pyinfra.api.util.open', fake_open, create=True):
564+
status = host.put_file(
565+
'not-a-file', 'not-another-file',
566+
print_output=True,
567+
su_user='centos',
568+
)
569+
570+
assert status is False
571+
572+
fake_ssh_client().exec_command.assert_any_call((
573+
"sh -c 'setfacl -m u:centos:r "
574+
"/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487'"
575+
), get_pty=False)
576+
577+
fake_ssh_client().exec_command.assert_called_with((
578+
"su centos -c 'sh -c '\"'\"'cp "
544579
'/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487 '
545-
"not-another-file && chown centos not-another-file'\"'\"''"
580+
"not-another-file'\"'\"''"
546581
), get_pty=False)
547582

548583
fake_sftp_client.from_transport().putfo.assert_called_with(

words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
4
77
64kb
88
700
9+
acl
910
addrs
1011
addsitedir
1112
adhoc

0 commit comments

Comments
 (0)