mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-02 18:17:11 +09:00
126 lines
4.4 KiB
Python
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'
|