Files
ServerKit/backend/tests/test_ssh_keys_roundtrip.py

126 lines
4.4 KiB
Python

"""authorized_keys list ↔ remove round trip.
get_ssh_keys numbered keys over raw file lines (comments and blanks included)
while remove_ssh_key popped that id from a list of key lines only, so any
comment banner in authorized_keys shifted every id and delete-by-id removed
the wrong key — the same write/read join-key mismatch as the cron #117 bug.
These tests pin the cycle: list → remove by the listed id → list again and
assert the right key went away and the file kept its shape.
"""
import pytest
from app.services.security_service import SecurityService
KEY_A = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeyAlicelicealice alice@laptop'
KEY_B = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeyBobbobbobbobbob bob@desktop'
KEY_C = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABKeyCarolcarolcarol carol@ci'
@pytest.fixture
def auth_keys(tmp_path, monkeypatch):
"""Point the service at a temp authorized_keys and stub the fingerprint
subprocess so no ssh-keygen is spawned."""
path = tmp_path / 'authorized_keys'
monkeypatch.setattr(SecurityService, 'SSH_DIR', str(tmp_path))
monkeypatch.setattr(SecurityService, 'AUTHORIZED_KEYS', str(path))
monkeypatch.setattr(
SecurityService, '_get_key_fingerprint',
classmethod(lambda cls, line: 'SHA256:' + line.split()[1][:8]))
return path
def _write(path, *lines):
path.write_text('\n'.join(lines) + '\n')
def test_ids_index_key_lines_not_file_lines(auth_keys):
_write(auth_keys, '# cloud-init banner', '', KEY_A, KEY_B)
result = SecurityService.get_ssh_keys()
assert result['success']
assert [k['id'] for k in result['keys']] == [0, 1]
assert result['keys'][0]['comment'] == 'alice@laptop'
def test_remove_by_listed_id_removes_that_key(auth_keys):
_write(auth_keys, '# cloud-init banner', KEY_A, KEY_B, KEY_C)
listed = SecurityService.get_ssh_keys()['keys']
alice = next(k for k in listed if k['comment'] == 'alice@laptop')
assert SecurityService.remove_ssh_key(alice['id'])['success']
content = auth_keys.read_text()
assert 'alice@laptop' not in content
assert 'bob@desktop' in content
assert 'carol@ci' in content
def test_remove_last_listed_key_is_valid(auth_keys):
# With the old raw-line ids, the banner made the last key's id exceed
# len(key_lines) and delete answered "Invalid key ID".
_write(auth_keys, '# banner one', '# banner two', KEY_A, KEY_B)
last = SecurityService.get_ssh_keys()['keys'][-1]
result = SecurityService.remove_ssh_key(last['id'])
assert result['success'], result
assert 'bob@desktop' not in auth_keys.read_text()
def test_remove_preserves_comment_positions(auth_keys):
_write(auth_keys, '# alice below', KEY_A, '# bob below', KEY_B)
assert SecurityService.remove_ssh_key(0)['success']
assert auth_keys.read_text().splitlines() == [
'# alice below', '# bob below', KEY_B]
def test_ids_stay_joined_across_removals(auth_keys):
_write(auth_keys, '# banner', KEY_A, KEY_B, KEY_C)
for victim in ('bob@desktop', 'carol@ci', 'alice@laptop'):
key = next(k for k in SecurityService.get_ssh_keys()['keys']
if k['comment'] == victim)
assert SecurityService.remove_ssh_key(key['id'])['success']
assert victim not in auth_keys.read_text()
assert SecurityService.get_ssh_keys()['keys'] == []
def test_add_appends_cleanly_without_trailing_newline(auth_keys):
auth_keys.write_text(KEY_A) # no trailing newline
assert SecurityService.add_ssh_key(KEY_B)['success']
lines = auth_keys.read_text().splitlines()
assert lines == [KEY_A, KEY_B]
def test_add_duplicate_matches_key_data_not_substring(auth_keys):
_write(auth_keys, KEY_A)
dup = SecurityService.add_ssh_key(KEY_A + ' renamed@host')
assert not dup['success']
# A different key must not be rejected just because its text shares a
# prefix with an existing line.
assert SecurityService.add_ssh_key(KEY_B)['success']
def test_add_duplicate_finds_key_data_after_authorized_key_options(auth_keys):
restricted = (
'from="10.0.0.0/8",command="echo backup only" '
f'{KEY_A}'
)
_write(auth_keys, restricted)
listed = SecurityService.get_ssh_keys()['keys']
assert listed[0]['type'] == 'ssh-ed25519'
assert listed[0]['comment'] == 'alice@laptop'
duplicate = SecurityService.add_ssh_key(KEY_A + ' renamed@host')
assert not duplicate['success']
assert duplicate['error'] == 'Key already exists'