mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-03-11 09:04:54 +00:00
backups: Use selected SSH credential for remote
- Use javascript to disable or enable password fields. - If SSH key auth is selected, then try the connection. - If SSH password auth is selected, then copy the key. Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
3558a26b2f
commit
043bd44dec
@ -251,15 +251,34 @@ class AddRemoteRepositoryForm(EncryptedBackupsMixin, forms.Form):
|
||||
help_text=_('Path of a new or existing repository. Example: '
|
||||
'<i>user@host:~/path/to/repo/</i>'),
|
||||
validators=[repository_validator])
|
||||
ssh_auth_type = forms.ChoiceField(
|
||||
label=_('SSH Authentication Type'),
|
||||
help_text=_('How to authenticate to the remote SSH server.<br />'
|
||||
'If Key-based Authentication is selected, then the '
|
||||
"FreedomBox service's SSH client public key must be added"
|
||||
" to the authorized keys list on the remote server.<br />"
|
||||
'If Password-based Authentication is selected, then '
|
||||
'FreedomBox will attempt to copy its SSH client public '
|
||||
'key to the remote server.'), widget=forms.RadioSelect(),
|
||||
choices=[('key_auth', _('Key-based Authentication')),
|
||||
('password_auth', _('Password-based Authentication'))])
|
||||
ssh_password = forms.CharField(
|
||||
label=_('SSH server password'), strip=True,
|
||||
help_text=_('Password of the SSH Server.<br />'
|
||||
'Either provide a password, or add the FreedomBox '
|
||||
"service's SSH client public key (listed above) to the "
|
||||
'authorized keys list on the remote machine.'),
|
||||
widget=forms.PasswordInput(), required=False)
|
||||
label=_('SSH server password'), widget=forms.PasswordInput(),
|
||||
strip=True, help_text=_(
|
||||
'Password of the SSH Server. Required only for Password-based '
|
||||
'Authentication.'), required=False)
|
||||
|
||||
field_order = ['repository', 'ssh_password'] + encryption_fields
|
||||
field_order = ['repository', 'ssh_auth_type', 'ssh_password'
|
||||
] + encryption_fields
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
ssh_password = self.cleaned_data.get('ssh_password')
|
||||
if self.cleaned_data.get(
|
||||
'ssh_auth_type') == 'password_auth' and not ssh_password:
|
||||
raise forms.ValidationError(
|
||||
_('SSH password is needed for password-based authentication.'))
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_repository(self):
|
||||
"""Validate repository form field."""
|
||||
|
||||
@ -335,6 +335,14 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def ssh_keyfile(self) -> str | None:
|
||||
"""Return path to SSH client key if stored, otherwise None."""
|
||||
if 'ssh_keyfile' in self.credentials:
|
||||
return self.credentials['ssh_keyfile']
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def _mountpoint(self):
|
||||
"""Return the local mount point where repository is to be mounted."""
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/**
|
||||
* @licstart The following is the entire license notice for the JavaScript
|
||||
* code in this page.
|
||||
*
|
||||
* This file is part of FreedomBox.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @licend The above is the entire license notice for the JavaScript code
|
||||
* in this page.
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const keyAuth = document.getElementById('id_ssh_auth_type_0');
|
||||
const passwordAuth = document.getElementById('id_ssh_auth_type_1');
|
||||
const sshPasswordField = document.getElementById('id_ssh_password');
|
||||
const encryptionType = document.getElementById('id_encryption');
|
||||
const encryptionPassphraseField = document.getElementById('id_encryption_passphrase');
|
||||
const encryptionConfirmPassphraseField = document.getElementById('id_confirm_encryption_passphrase');
|
||||
|
||||
function handleAuthTypeChange() {
|
||||
if (keyAuth.checked) {
|
||||
sshPasswordField.value = "";
|
||||
sshPasswordField.disabled = true;
|
||||
} else {
|
||||
sshPasswordField.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEncryptionTypeChange() {
|
||||
if (encryptionType.value === "repokey") {
|
||||
encryptionPassphraseField.disabled = false;
|
||||
encryptionConfirmPassphraseField.disabled = false;
|
||||
} else {
|
||||
encryptionPassphraseField.value = "";
|
||||
encryptionPassphraseField.disabled = true;
|
||||
encryptionConfirmPassphraseField.value = "";
|
||||
encryptionConfirmPassphraseField.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
keyAuth.addEventListener('change', handleAuthTypeChange);
|
||||
passwordAuth.addEventListener('change', handleAuthTypeChange);
|
||||
encryptionType.addEventListener('change', handleEncryptionTypeChange);
|
||||
|
||||
handleAuthTypeChange();
|
||||
handleEncryptionTypeChange();
|
||||
});
|
||||
@ -5,6 +5,12 @@
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'backups/backups_add_remote_repository.js' %}"
|
||||
defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
@ -385,10 +385,13 @@ class AddRemoteRepositoryView(FormView):
|
||||
if form.cleaned_data.get('encryption') == 'none':
|
||||
encryption_passphrase = None
|
||||
|
||||
credentials = {
|
||||
'ssh_password': form.cleaned_data.get('ssh_password'),
|
||||
'encryption_passphrase': encryption_passphrase
|
||||
}
|
||||
credentials = {'encryption_passphrase': encryption_passphrase}
|
||||
if form.cleaned_data.get('ssh_auth_type') == 'password_auth':
|
||||
credentials['ssh_password'] = form.cleaned_data.get('ssh_password')
|
||||
else:
|
||||
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
||||
credentials['ssh_keyfile'] = str(key_path)
|
||||
|
||||
with handle_common_errors(self.request):
|
||||
repository = SshBorgRepository(path, credentials)
|
||||
repository.verfied = False
|
||||
@ -437,42 +440,70 @@ class VerifySshHostkeyView(FormView):
|
||||
with known_hosts_path.open('a', encoding='utf-8') as known_hosts_file:
|
||||
known_hosts_file.write(ssh_public_key + '\n')
|
||||
|
||||
def _check_save_repository(self):
|
||||
"""Save the repository and redirect according to the result."""
|
||||
if _save_repository(self.request, self._get_repository()):
|
||||
return redirect(reverse_lazy('backups:index'))
|
||||
|
||||
return redirect(reverse_lazy('backups:add-remote-repository'))
|
||||
|
||||
def _check_copy_ssh_client_public_key(self):
|
||||
""" Try to copy FreedomBox's SSH client public key to the host."""
|
||||
"""Try to copy FreedomBox's SSH client public key to the host."""
|
||||
repo = self._get_repository()
|
||||
result, message = copy_ssh_client_public_key(repo.hostname,
|
||||
repo.username,
|
||||
repo.ssh_password)
|
||||
if result:
|
||||
logger.info(
|
||||
"Copied SSH client public key to remote host's authorized "
|
||||
"keys.")
|
||||
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
||||
repo.replace_ssh_password_with_keyfile(str(key_path))
|
||||
if _save_repository(self.request, repo):
|
||||
return redirect(reverse_lazy('backups:index'))
|
||||
else:
|
||||
ssh_password = repo.ssh_password
|
||||
if ssh_password:
|
||||
result, message = copy_ssh_client_public_key(
|
||||
repo.hostname, repo.username, repo.ssh_password)
|
||||
if result:
|
||||
logger.info("Copied SSH client public key to remote host's "
|
||||
"authorized keys.")
|
||||
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
||||
repo.replace_ssh_password_with_keyfile(str(key_path))
|
||||
return self._check_save_repository()
|
||||
|
||||
logger.warning('Failed to copy SSH client public key: %s', message)
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Failed to copy SSH client public key: %s') % message)
|
||||
# Remove the repository so that the user can have another go at
|
||||
# creating it.
|
||||
try:
|
||||
repo.remove()
|
||||
messages.error(self.request, _('Repository removed.'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
else:
|
||||
logger.error(
|
||||
'SSH password is required to copy SSH client public key.')
|
||||
messages.error(
|
||||
self.request,
|
||||
_('SSH password is required to copy SSH public key.'))
|
||||
|
||||
# Remove the repository so that the user can have another go at
|
||||
# creating it.
|
||||
try:
|
||||
repo.remove()
|
||||
messages.error(self.request, _('Repository removed.'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return redirect(reverse_lazy('backups:add-remote-repository'))
|
||||
|
||||
def _check_next_step(self):
|
||||
"""Check whether we need to copy the SSH client public key.
|
||||
|
||||
Otherwise, save the repository and redirect.
|
||||
"""
|
||||
if self._get_repository().ssh_keyfile:
|
||||
# SSH keyfile credential is stored. Assume it is already copied to
|
||||
# the remote host. Check the connection.
|
||||
logger.info('Check connection using SSH keyfile...')
|
||||
return self._check_save_repository()
|
||||
|
||||
logger.info('Copy SSH client public key to remote host...')
|
||||
return self._check_copy_ssh_client_public_key()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""Skip this view if host is already verified."""
|
||||
if not is_ssh_hostkey_verified(self._get_repository().hostname):
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
messages.success(self.request, _('SSH host already verified.'))
|
||||
return self._check_copy_ssh_client_public_key()
|
||||
return self._check_next_step()
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Create and store the repository."""
|
||||
@ -480,7 +511,8 @@ class VerifySshHostkeyView(FormView):
|
||||
with handle_common_errors(self.request):
|
||||
self._add_ssh_hostkey(ssh_public_key)
|
||||
messages.success(self.request, _('SSH host verified.'))
|
||||
return self._check_copy_ssh_client_public_key()
|
||||
|
||||
return self._check_next_step()
|
||||
|
||||
|
||||
def _save_repository(request, repository):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user