mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +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: '
|
help_text=_('Path of a new or existing repository. Example: '
|
||||||
'<i>user@host:~/path/to/repo/</i>'),
|
'<i>user@host:~/path/to/repo/</i>'),
|
||||||
validators=[repository_validator])
|
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(
|
ssh_password = forms.CharField(
|
||||||
label=_('SSH server password'), strip=True,
|
label=_('SSH server password'), widget=forms.PasswordInput(),
|
||||||
help_text=_('Password of the SSH Server.<br />'
|
strip=True, help_text=_(
|
||||||
'Either provide a password, or add the FreedomBox '
|
'Password of the SSH Server. Required only for Password-based '
|
||||||
"service's SSH client public key (listed above) to the "
|
'Authentication.'), required=False)
|
||||||
'authorized keys list on the remote machine.'),
|
|
||||||
widget=forms.PasswordInput(), 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):
|
def clean_repository(self):
|
||||||
"""Validate repository form field."""
|
"""Validate repository form field."""
|
||||||
|
|||||||
@ -335,6 +335,14 @@ class SshBorgRepository(BaseBorgRepository):
|
|||||||
|
|
||||||
return None
|
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
|
@property
|
||||||
def _mountpoint(self):
|
def _mountpoint(self):
|
||||||
"""Return the local mount point where repository is to be mounted."""
|
"""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 bootstrap %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block page_js %}
|
||||||
|
<script type="text/javascript" src="{% static 'backups/backups_add_remote_repository.js' %}"
|
||||||
|
defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|||||||
@ -385,10 +385,13 @@ class AddRemoteRepositoryView(FormView):
|
|||||||
if form.cleaned_data.get('encryption') == 'none':
|
if form.cleaned_data.get('encryption') == 'none':
|
||||||
encryption_passphrase = None
|
encryption_passphrase = None
|
||||||
|
|
||||||
credentials = {
|
credentials = {'encryption_passphrase': encryption_passphrase}
|
||||||
'ssh_password': form.cleaned_data.get('ssh_password'),
|
if form.cleaned_data.get('ssh_auth_type') == 'password_auth':
|
||||||
'encryption_passphrase': encryption_passphrase
|
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):
|
with handle_common_errors(self.request):
|
||||||
repository = SshBorgRepository(path, credentials)
|
repository = SshBorgRepository(path, credentials)
|
||||||
repository.verfied = False
|
repository.verfied = False
|
||||||
@ -437,42 +440,70 @@ class VerifySshHostkeyView(FormView):
|
|||||||
with known_hosts_path.open('a', encoding='utf-8') as known_hosts_file:
|
with known_hosts_path.open('a', encoding='utf-8') as known_hosts_file:
|
||||||
known_hosts_file.write(ssh_public_key + '\n')
|
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):
|
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()
|
repo = self._get_repository()
|
||||||
result, message = copy_ssh_client_public_key(repo.hostname,
|
ssh_password = repo.ssh_password
|
||||||
repo.username,
|
if ssh_password:
|
||||||
repo.ssh_password)
|
result, message = copy_ssh_client_public_key(
|
||||||
if result:
|
repo.hostname, repo.username, repo.ssh_password)
|
||||||
logger.info(
|
if result:
|
||||||
"Copied SSH client public key to remote host's authorized "
|
logger.info("Copied SSH client public key to remote host's "
|
||||||
"keys.")
|
"authorized keys.")
|
||||||
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
||||||
repo.replace_ssh_password_with_keyfile(str(key_path))
|
repo.replace_ssh_password_with_keyfile(str(key_path))
|
||||||
if _save_repository(self.request, repo):
|
return self._check_save_repository()
|
||||||
return redirect(reverse_lazy('backups:index'))
|
|
||||||
else:
|
|
||||||
logger.warning('Failed to copy SSH client public key: %s', message)
|
logger.warning('Failed to copy SSH client public key: %s', message)
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_('Failed to copy SSH client public key: %s') % message)
|
_('Failed to copy SSH client public key: %s') % message)
|
||||||
# Remove the repository so that the user can have another go at
|
|
||||||
# creating it.
|
else:
|
||||||
try:
|
logger.error(
|
||||||
repo.remove()
|
'SSH password is required to copy SSH client public key.')
|
||||||
messages.error(self.request, _('Repository removed.'))
|
messages.error(
|
||||||
except KeyError:
|
self.request,
|
||||||
pass
|
_('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'))
|
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):
|
def get(self, *args, **kwargs):
|
||||||
"""Skip this view if host is already verified."""
|
"""Skip this view if host is already verified."""
|
||||||
if not is_ssh_hostkey_verified(self._get_repository().hostname):
|
if not is_ssh_hostkey_verified(self._get_repository().hostname):
|
||||||
return super().get(*args, **kwargs)
|
return super().get(*args, **kwargs)
|
||||||
|
|
||||||
messages.success(self.request, _('SSH host already verified.'))
|
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):
|
def form_valid(self, form):
|
||||||
"""Create and store the repository."""
|
"""Create and store the repository."""
|
||||||
@ -480,7 +511,8 @@ class VerifySshHostkeyView(FormView):
|
|||||||
with handle_common_errors(self.request):
|
with handle_common_errors(self.request):
|
||||||
self._add_ssh_hostkey(ssh_public_key)
|
self._add_ssh_hostkey(ssh_public_key)
|
||||||
messages.success(self.request, _('SSH host verified.'))
|
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):
|
def _save_repository(request, repository):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user