diff --git a/community_backup/tests/__init__.py b/community_backup/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/community_backup/tests/test_models.py b/community_backup/tests/test_models.py new file mode 100644 index 0000000..74e2aa4 --- /dev/null +++ b/community_backup/tests/test_models.py @@ -0,0 +1,54 @@ +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from webui.models import BorgRepository + + +class BorgRepositoryTestCase(TestCase): + def test_ssh_key_regex(self): + user = User.objects.create(username="foo") + + # good repo + repo = BorgRepository.objects.create( + name="test1", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + + repo.full_clean() + + # good repo, key without comment + repo = BorgRepository.objects.create( + name="test2", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3", + user=user, + ) + repo.full_clean() + + # bad repo (wrong key) + repo = BorgRepository.objects.create(name="test3", key="Foo", user=user) + self.assertRaises(ValidationError, repo.full_clean) + + # bad repo (multiple valid keys ) + repo = BorgRepository.objects.create( + name="test4", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + self.assertRaises(ValidationError, repo.full_clean) + + # bad repo, leading whitespace + repo = BorgRepository.objects.create( + name="test5", + key=" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + self.assertRaises(ValidationError, repo.full_clean) + + # good repo, with whitespace in commment + repo = BorgRepository.objects.create( + name="test6", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris whitespace in comment", + user=user, + ) + repo.full_clean() diff --git a/community_backup/webui/deployments.py b/community_backup/webui/deployments.py index b56d1bc..527dda3 100644 --- a/community_backup/webui/deployments.py +++ b/community_backup/webui/deployments.py @@ -8,8 +8,6 @@ from django.conf import settings logger = getLogger("deployments") -# sync users - def sync_repos(dry_run=False): """Synchronize the repos""" @@ -19,7 +17,7 @@ def sync_repos(dry_run=False): repos_by_key = defaultdict(list) for repo in repos: - repos_by_key[repo.key].append(repo) + repos_by_key[f"{repo.key_type} {repo.key_value}"].append(repo) # create .ssh directory ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent diff --git a/community_backup/webui/forms.py b/community_backup/webui/forms.py index 99e774c..3ac0cb0 100644 --- a/community_backup/webui/forms.py +++ b/community_backup/webui/forms.py @@ -10,7 +10,7 @@ class BorgRepositoryForm(forms.ModelForm): class Meta: fields = ["name", "key", "user"] model = BorgRepository - widgets = {"user": forms.HiddenInput()} + widgets = {"user": forms.HiddenInput(), "key": forms.TextInput()} class RegisterUserForm(UserCreationForm): diff --git a/community_backup/webui/models.py b/community_backup/webui/models.py index 75c1962..d04785a 100644 --- a/community_backup/webui/models.py +++ b/community_backup/webui/models.py @@ -21,7 +21,7 @@ class BorgRepository(models.Model): key = models.TextField( validators=[ RegexValidator( - r"(ssh\-rsa|ecdsa\-sha2\-nistp256|ssh\-ed25519) ([a-zA-Z0-9\+/=]+) (\S*)", + r"^(ssh\-rsa|ecdsa\-sha2\-nistp256|ssh\-ed25519) ([a-zA-Z0-9\+/=]+)( ([\S ]*))?$", message="not a valid SSH public key.", ), ] @@ -46,9 +46,9 @@ class BorgRepository(models.Model): def repo_url(self) -> str: return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}" - def save(self): + def save(self, *args, **kwargs): - super().save() + super().save(*args, **kwargs) update_user.enqueue() def delete(self): @@ -56,6 +56,45 @@ class BorgRepository(models.Model): super().delete() update_user.enqueue() + @property + def key_type(self) -> str: + return self.key.split(" ", 1)[0].strip() + + @property + def key_value(self) -> str: + return self.key.split(" ", 2)[1].strip() + + @property + def key_comment(self) -> str | None: + splits = self.key.split(" ", 2) + if len(splits) == 3: + return splits[2].strip() + return None + + def truncated_key(self, length: int = 60): + """ + Returns a truncated version of the key for display purposes. + The Key type and the comment are retained as much as possible. + """ + + # check how long the truncated part of the key may be at most. + max_len = length - len(self.key_type) - 1 + if self.key_comment: + max_len = max_len - len(self.key_comment) - 1 + + if len(self.key_value) > max_len: + max_len -= 1 # remove one character if we add an ellipsis + trunc_key = self.key_value[:max_len] + "…" + else: + trunc_key = self.key_value + + output = f"{self.key_type} {trunc_key}" + + if self.key_comment: + output += f" {self.key_comment}" + + return output + class Voucher(models.Model): used = models.BooleanField(default=False) diff --git a/community_backup/webui/templates/borg_list.html b/community_backup/webui/templates/borg_list.html index b193fcd..cd508d4 100644 --- a/community_backup/webui/templates/borg_list.html +++ b/community_backup/webui/templates/borg_list.html @@ -24,7 +24,7 @@ {% for repo in object_list %}
{{ repo.key | truncatechars:40 }}{{ repo.truncated_key }}{{ repo.repo_url}}