freedombox release 26.4 for unstable

[dgit distro=debian split]
 [dgit please-upload source=freedombox version=26.4]
 -----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmmmURoWHGp2YWxsZXJv
 eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICG64EACp4jhGnaOQKlfPEyHRftaDYCon
 UgStrP3M2zPDjWBiHKpgjQiUJuhQaQZJIeME5k83IcP3MC3KrK3Q4nrYj28viPeS
 S/O0aOn7brQT5rVP7OkElkfJONJlaOpLVz/TXYSUf3RIew5cGVjE8SBdlq7R8OrB
 T30IAlqvqXjutK6lcOGfstXO3kcpAPvHchroZ4ROxGkmJorF1l/1DdLFJHsfHkGE
 7UvOne+asn5ZKku11tV1ENSeseie0FBi8G1+tez2Rwb981ffABXTulJ/ypxX4VpK
 rLfJ9vlINfYCWObqvcaT9L7obEePvJVnH7MQvSbaT3FufocgrSlNquZZ50MAe72F
 EUJQMDnLVP859Gkd7YUNwUDs31Mw/OM1/nVG1SoY4r5kBrFJrW7oCmqpkEci3X0l
 QQ+QcxmnBJep0pT+oVY2bBPsIpLE4X6Q/pPCccPa3rA4kbqJPSzVGaplJ+2qdROv
 T+sJpjQ+CITGzQQRKF/OP1ZxF0m03TFNUEOE2r0YECMjF22ez0i3LG+LXRoH09p+
 j4PeE8a67R+YjgMS0QG+L5hID4RQGgENeSSM/PU4isAZ1wE1V4r+EfT0PlH8HQpM
 EN6QGipkL5jRmFxBK+A34wpnlNNuBde78XrGgkFKB23B8HaQWm/h1lVQqpUfe8sO
 r2jjc6mKWg0cVm8oZg==
 =20uS
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmmrOi4WHGp2YWxsZXJv
 eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICPyHEACGLGATAQrWIIuDnCDQWcAe//Lw
 cW77Wr1UPlJIoVSy5fuDfMbtTqpSeYMxqhw+Di2e3jjDThys9z2XJVjoX9p5cmJC
 L3wvDQsrq77EBgDMMHclCo1VhVUaZtOVO5ytSEgya3AtIdo5JwtMQif3TuLwpaVt
 KPrZ0WRO7zSmAA4m7SiDYS2G6Nvid9bXN8X4PMQc4CvPuF5PYF4KeO83ERr2EVbk
 QrnX28fE4o3xd1YV+mrO6olOG8ryJzts6Wfd5QAqbybi62II69s1eKDTV2blpBsY
 IJu68kWnkne6NetB49VHFCyRSd3E4w9lDvUiaUy482XKzC2vJiJ30SPzK52Vs6vP
 +VNpMa+s1JnjAECzLVkowkDkO7PWBX+2mNuFpugIP1yzp/M9IEBPQ2T+QsrILjf3
 RyYKCzjdKTD0QSvN0ZF5xUtpJM0g6MBMNt8q6Kol1ZBnaU6Ezs2etgwHKX7/DvA4
 lp8zpbxW89MCxiKb5w7KSM0+78914B5U9pdwWu6qgXBv9woMsRXo08jlON/PTYdd
 LlZUYspBzto523ge6ysyW+ix8DwEF9spJejJIfptGAr+stugvScEyZyv+4NJjBAE
 +cREE0uPZjlPJrjZHwIPOYBkSRST4CNvVW2aVGGwTaJJYE3aL2EEi8L6pNpZI6a3
 KhS4DAvVjrK/lCBmdg==
 =AQUo
 -----END PGP SIGNATURE-----

Merge tag 'debian/26.4' into debian/trixie-backports

freedombox release 26.4 for unstable
This commit is contained in:
James Valleroy 2026-03-06 15:33:35 -05:00
commit 9e795ee8d7
235 changed files with 23824 additions and 13929 deletions

View File

@ -1,16 +0,0 @@
#!/usr/bin/python3
# -*- mode: python -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Set required permissions for user "plinth" to run plinth in dev setup."""
import pathlib
content = '''
Cmnd_Alias FREEDOMBOX_ACTION_DEV = /usr/share/plinth/actions/actions, /freedombox/actions/actions
Defaults!FREEDOMBOX_ACTION_DEV closefrom_override
plinth ALL=(ALL:ALL) NOPASSWD:SETENV : FREEDOMBOX_ACTION_DEV
fbx ALL=(ALL:ALL) NOPASSWD : ALL
'''
sudoers_file = pathlib.Path('/etc/sudoers.d/01-freedombox-development')
sudoers_file.write_text(content)

View File

@ -66,7 +66,7 @@ development environment inside a systemd-nspawn container.
folder: (This step requires at least 16GB of free disk space)
```bash
host$ ./container up
host$ ./container start
```
1. To run unit tests:
@ -97,20 +97,20 @@ development environment inside a systemd-nspawn container.
1. Using an environment variable.
```bash
host$ DISTRIBUTION=stable ./container up
host$ DISTRIBUTION=stable ./container start
host$ DISTRIBUTION=stable ./container ssh
```
```bash
host$ export DISTRIBUTION=stable
host$ ./container up
host$ ./container start
host$ ./container ssh
```
2. Using the `--distribution` option for each command.
```bash
host$ ./container up --distribution=stable
host$ ./container start --distribution=stable
host$ ./container ssh --distribution=stable
```
@ -131,7 +131,7 @@ used simultaneously as they all use different disk images.
example, to bring up a virtual machine instead of a container run:
```bash
host$ ./container up --machine-type=vm
host$ ./container start --machine-type=vm
```
#### Using after Setup
@ -164,9 +164,9 @@ Note: This development container has automatic upgrades disabled by default.
#### Troubleshooting
* Sometimes `host$ ./container destroy && ./container up` doesn't work. In such
* Sometimes `host$ ./container destroy && ./container start` doesn't work. In such
cases, try to delete the hidden `.container` folder and then `host$
./container up`.
./container start`.
* Not all kinds of changes are automatically updated. Try `guest$ sudo mount -o
remount /freedombox`.
* I am getting an error that says `lo` is not managed by Network Manager
@ -178,7 +178,7 @@ Note: This development container has automatic upgrades disabled by default.
```bash
host$ sudo touch /etc/NetworkManager/conf.d/10-globally-managed-devices.conf
host$ sudo service network-manager restart
host$ ./container destroy && ./container up
host$ ./container destroy && ./container start
```
* File/directory not found errors when running tests can be fixed by clearing `__pycache__` directories.
@ -416,7 +416,7 @@ for more details.
### Translating literals (contributing translations)
The easiest way to start translating is with your browser, by using
[Weblate](https://hosted.weblate.org/projects/freedombox/plinth/).
[Weblate](https://hosted.weblate.org/projects/freedombox/freedombox/).
Your changes will automatically get pushed to the code repository.
Alternatively, you can directly edit the `.po` file in your language directory

View File

@ -35,7 +35,7 @@ FreedomBox [Manual](https://wiki.debian.org/FreedomBox/Manual/)'s
3. Access FreedomBox UI:
UI should be accessible at http://localhost:8000/plinth
UI should be accessible at http://localhost:8000/freedombox
If you are installing FreedomBox Service (Plinth) for development purposes, see
HACKING.md instead.

View File

@ -21,7 +21,8 @@ DISABLED_APPS_TO_REMOVE := \
tahoe \
mldonkey \
i2p \
ttrss
ttrss \
sso
APP_FILES_TO_REMOVE := $(foreach app,$(DISABLED_APPS_TO_REMOVE),$(ENABLED_APPS_PATH)/$(app))
@ -101,11 +102,12 @@ install:
$(INSTALL) -d $(DESTDIR)$${lib_dir} && \
rm -rf $(DESTDIR)$${lib_dir}/plinth $(DESTDIR)$${lib_dir}/plinth*.dist-info && \
mv $${temp}/plinth $${temp}/plinth*.dist-info $(DESTDIR)$${lib_dir} && \
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/licenses/COPYING.md && \
rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \
$(INSTALL) -D -t $(BIN_DIR) bin/plinth
$(INSTALL) -D -t $(LIB_DIR)/freedombox bin/freedombox-privileged
$(INSTALL) -D -t $(BIN_DIR) bin/freedombox-cmd
$(INSTALL) -D -t $(BIN_DIR) bin/freedombox-change-password
# Static web server files
rm -rf $(STATIC_FILES_DIRECTORY)
@ -229,7 +231,7 @@ provision-dev:
sshpass bash-completion
wait-while-first-setup:
while [ x$$(curl -k https://localhost/plinth/status/ 2> /dev/null | \
while [ x$$(curl -k https://localhost/freedombox/status/ 2> /dev/null | \
json_pp 2> /dev/null | grep 'is_first_setup_running' | \
tr -d '[:space:]' | cut -d':' -f2 ) != 'xfalse' ] ; do \
sleep 1; echo -n .; done

8
Vagrantfile vendored
View File

@ -28,17 +28,11 @@ Vagrant.configure(2) do |config|
SHELL
config.vm.provision "tests", run: "never", type: "shell", path: "plinth/tests/functional/install.sh"
config.vm.post_up_message = "FreedomBox virtual machine is ready
for development. Plinth will be available at https://localhost:4430/plinth
for development. Plinth will be available at https://localhost:4430/freedombox
(with an invalid SSL certificate). To watch logs:
$ vagrant ssh
$ sudo freedombox-logs
"
config.trigger.after [:up, :resume, :reload] do |trigger|
trigger.info = "Set plinth user permissions for development environment"
trigger.run_remote = {
path: ".vagrant-scripts/plinth-user-permissions.py"
}
end
config.vm.boot_timeout=1200
end

61
bin/freedombox-change-password Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Utility to change user password in FreedomBox's Django database.
Usage:
$ freedombox-change-password <username>
"""
import argparse
import getpass
import sys
import plinth.web_framework
from plinth.modules.users import privileged
def main():
"""Ask for new password, setup Django and update a user's password."""
try:
plinth.web_framework.init()
except Exception:
_print('Error initializing Django.')
return
parser = argparse.ArgumentParser()
parser.add_argument('username',
help='Username of the account to change password for')
args = parser.parse_args()
username = args.username
password = getpass.getpass('Enter new password: ')
try:
_change_password(username, password)
privileged._set_user_password(username, password)
privileged._set_samba_user(username, password)
_print('Password updated in web interface, LDAP, and samba databases.')
except Exception as exception:
_print('Error setting password:', str(exception))
def _print(*args, **kwargs):
"""Write to stderr."""
print(*args, **kwargs, file=sys.stderr)
def _change_password(username: str, password: str):
"""Update the password in SQLite database file."""
from django.contrib.auth.models import User
try:
user = User.objects.get(username=username)
user.set_password(password)
user.save()
except User.DoesNotExist:
_print('User account does not exist:', username)
raise
if __name__ == '__main__':
main()

View File

@ -1064,7 +1064,7 @@ Terminal login : sudo machinectl login fbx-{distribution}
Open a root shell : sudo machinectl shell fbx-{distribution}
Shutdown : {script} stop {options}
Destroy : {script} destroy {options}
Reset : {script} destroy {options}; {script} up {options}'''
Reset : {script} destroy {options}; {script} start {options}'''
logger.info(message)

View File

@ -12,6 +12,7 @@
# Don't redirect for onion sites as it is not needed and leads to
# unnecessary warning.
RewriteCond %{HTTP_HOST} !^.*\.onion$ [NC]
RewriteCond %{REQUEST_URI} !^/freedombox/apache/discover-idp/$ [NC]
ReWriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</LocationMatch>

View File

@ -39,16 +39,16 @@
</If>
##
## Redirect traffic on home to /plinth as part of turning the machine
## Redirect traffic on home to /freedombox as part of turning the machine
## into FreedomBox server. Plinth then acts as a portal to reach all
## other services.
##
<IfFile !/etc/apache2/conf-enabled/freedombox-apache-homepage.conf>
RedirectMatch "^/$" "/plinth"
RedirectMatch "^/$" "/freedombox"
</IfFile>
##
## On all sites, provide FreedomBox on a default path: /plinth
## On all sites, provide FreedomBox on a default path: /freedombox
##
## Requires the following Apache modules to be enabled:
## mod_headers
@ -56,7 +56,8 @@
## mod_proxy_http
##
<Location /freedombox>
ProxyPass http://127.0.0.1:8000/plinth
ProxyPass http://127.0.0.1:8000/freedombox
ProxyPreserveHost On
## Send the scheme from user's request to enable Plinth to redirect
## URLs, set cookies, set absolute URLs (if any) properly.
RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
@ -70,7 +71,20 @@
RequestHeader unset X-Forwarded-For
</Location>
<Location /plinth>
ProxyPass http://127.0.0.1:8000/plinth
ProxyPass http://127.0.0.1:8000/freedombox
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
RequestHeader unset X-Forwarded-For
</Location>
<Location /.well-known/openid-configuration>
ProxyPass http://127.0.0.1:8000/freedombox/o/.well-known/openid-configuration
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
RequestHeader unset X-Forwarded-For
</Location>
<Location /.well-known/jwks.json>
ProxyPass http://127.0.0.1:8000/freedombox/o/.well-known/jwks.json
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
RequestHeader unset X-Forwarded-For
</Location>
@ -82,7 +96,7 @@
<Location ~ ^/favicon\.ico$>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule /favicon\.ico$ "/plinth/static/theme/img/favicon.ico" [PT]
RewriteRule /favicon\.ico$ "/freedombox/static/theme/img/favicon.ico" [PT]
</IfModule>
</Location>

183
debian/changelog vendored
View File

@ -1,3 +1,186 @@
freedombox (26.4) unstable; urgency=medium
[ Joseph Nuthalapati ]
* ui: Dismiss notifications without page reload
[ Sunil Mohan Adapa ]
* ui: Refactor notification delete buttons to avoid repeating code
* ui: Add animation for notification dismissal
* actions, privileged_daemon: Drop some unused global statements
* backups: Avoid some repeated text in form help text
* backups: Fix issue with Javascript in add remote location form
* backups: Show/hide form elements instead of disabling for simplicity
* backups: Tweak appearance of add remote location form
* backups: tests: Simplify functional test using more classes
* backups: Minor refactoring
* backups: Simplify handling of migration to SSH keys
* backups: Create .ssh folder before creating SSH key
* backups: Fix showing proper error for incorrect passphrase
* backups: Create a better comment in the generated SSH key file
* backups: Fix type checking errors
* action_utils: Implement utility to change umask temporarily
* quassel: Explicitly set permissions on the domain configuration file
* letsencrypt: When copying certificate reset the umask reliably
* doc/dev: Set new theme for developer documentation
* action_utils: Fix issue with type checking a generator
* tests: functional: Increase systemd rate limits for starting units
* js: When page load fails during install, show it to user
* tests: functional: Fix reloading error page during install/uninstall
* locale/de: Fix several translations with HTML links (German)
* locale/bg: Fix several translations with HTML links (Bulgarian)
* bin: Add tool to change FreedomBox password in Django database
* ejabberd: Fix setting up certificates for multiple domains
* gitweb: Fix issue with running post init due to missing method
* wireguard: Fix format when showing multiple endpoints of the server
* wireguard: Fix showing default route setting in server edit form
* wireguard: Show status of default route in server information page
* wireguard: Accept/use netmask with IP address for server connection
* README/HACKING: Update weblate project path to /freedombox
* *: Remove some absolute file paths in SVGs
* matrixsynapse: Update apache config to proxy Synapse client API
* cfg: Drop unused config_dir option
* cfg: Drop unused actions_dir option
* Vagrantfile: Drop unnecessary sudo configuration for actions
* pyproject: Use new format to specify licenses
* action_utils: Drop support for link-local IPv6 addresses
* debian: Ensure that gbp creates a clean tarball prior to build
* syncthing: tests: Fix tests by allowing rapid restarts
* web_server: Log requests to WSGI app
* *: Update URL base from /plinth to /freedombox
* tests: functional: Fix expecting FreedomBox to be home page
* web_framework: Allow FreedomBox apps to override templates
* templates: Allow building pages without navigation bar and footer
* apache: Preserve host header when proxying to service
* oidc: New app to implement OpenID Connect Provider
* oidc: Style the page for authorizing an OIDC app
* apache: Implement protecting apps using OpenID Connect
* featherwiki: Use OpenID Connect instead of pubtkt based SSO
* syncthing: Use OpenID Connect instead of pubtkt based SSO
* searx: Use OpenID Connect instead of pubtkt based SSO
* rssbridge: Use OpenID Connect instead of pubtkt based SSO
* email: Use OpenID Connect instead of pubtkt based SSO
* calibre: Use OpenID Connect instead of pubtkt based SSO
* deluge: Use OpenID Connect instead of pubtkt based SSO
* gitweb: Use OpenID Connect instead of pubtkt based SSO
* tiddlywiki: Use OpenID Connect instead of pubtkt based SSO
* wordpress: Use OpenID Connect instead of pubtkt based SSO when private
* transmission: Use OpenID Connect instead of pubtkt based SSO
* doc/dev: Use OpenID Connect instead of pubtkt based SSO
* sharing: Use OpenID Connect instead of pubtkt based SSO
* sso: Merge into users module, drop pubtkt related code
* apache: Fix diagnosing URLs protected by OpenID Connect
[ 大王叫我来巡山 ]
* Translated using Weblate (Chinese (Simplified Han script))
[ Burak Yavuz ]
* Translated using Weblate (Turkish)
[ Coucouf ]
* Translated using Weblate (French)
* Translated using Weblate (French)
[ Besnik Bleta ]
* Translated using Weblate (Albanian)
[ Dietmar ]
* Translated using Weblate (German)
[ James Valleroy ]
* backups: Generate SSH client key if needed
* backups: Display SSH public key when adding remote
* backups: Copy SSH client public key to remote
* backups: Use SSH key instead of password
* backups: Use selected SSH credential for remote
* backups: Test adding/removing remote location
* backups: Arrange form for adding remote location
* backups: Migrate to SSH key auth when mounting
* Translated using Weblate (Greek)
* mumble: murmurd renamed to mumble-server
* Translated using Weblate (Tamil)
* locale: Update translation strings
* doc: Fetch latest manual
* apache: Fix check_url test
[ Frederico Gomes ]
* container: Align terminology in printed banner
* wireguard: filter .local addresses from showClient view
* wireguard: improved server section UX flow
* wireguard: show server vpn ip in show client page
* wireguard: Fix split tunneling
* miniflux: Revert workaround for a packaging bug with DB connection
* db: Create a utility to get credentials from dbconfig
* miniflux: Get credentials from dbconfig-common directly
[ Pierfrancesco Passerini ]
* Translated using Weblate (Italian)
[ Daniel Wiik ]
* Translated using Weblate (Swedish)
* Translated using Weblate (Swedish)
[ kosagi ]
* Translated using Weblate (Catalan)
[ Jiří Podhorecký ]
* Translated using Weblate (Czech)
[ Isak ]
* Translated using Weblate (Swedish)
[ Βασίλης Χατζηκαμάρης ]
* Translated using Weblate (Greek)
[ Benedek Nagy ]
* doc/dev: always have an up-to-date copyright year
[ தமிழ்நேரம் ]
* Translated using Weblate (Tamil)
-- James Valleroy <jvalleroy@mailbox.org> Mon, 02 Mar 2026 21:35:46 -0500
freedombox (26.3) unstable; urgency=medium
[ Frederico Gomes ]
* wireguard: Add 'Start Server' button
* docs: Update container script usage
* wireguard: Show next available client IP in Add Client form
* wireguard: Show server endpoint on main app page
[ James Valleroy ]
* wireguard: Update functional tests to handle Start Server button
* lintian: Remove mismatched overrides
* Makefile: Fix removing extra license file
* debian: Follows policy 4.7.3
* debian: Remove default Rules-Requires-Root
* debian: Remove preinst script
* debian: Update copyright years
* locale: Update translation strings
* doc: Fetch latest manual
[ Ettore Atalan ]
* Translated using Weblate (German)
[ Sunil Mohan Adapa ]
* debian: Ignore lintian warning: service file missing Install section
* wireguard: Remove NM connections when app is uninstalled
* ui: Use HTMX to update notifications on partial page updates
[ Joseph Nuthalapati ]
* ui: Add HTMX as a dependency
* ui: Use HTMX to eliminate full page reloads
[ Burak Yavuz ]
* Translated using Weblate (Turkish)
[ Pierfrancesco Passerini ]
* Translated using Weblate (Italian)
[ 大王叫我来巡山 ]
* Translated using Weblate (Chinese (Simplified Han script))
-- James Valleroy <jvalleroy@mailbox.org> Mon, 02 Feb 2026 20:41:30 -0500
freedombox (26.2~bpo13+1) trixie-backports; urgency=medium
* Rebuild for trixie-backports.

8
debian/control vendored
View File

@ -1,6 +1,5 @@
Source: freedombox
Section: web
Priority: optional
Maintainer: FreedomBox packaging team <freedombox-pkg-team@lists.alioth.debian.org>
Uploaders:
Tzafrir Cohen <tzafrir@debian.org>,
@ -18,6 +17,7 @@ Build-Depends:
e2fsprogs,
gir1.2-nm-1.0,
libjs-bootstrap5,
libjs-htmx,
# Older libjs-bootstrap5 does not have proper dependency on popper.js >= 2.0
node-popper2,
pybuild-plugin-pyproject,
@ -35,6 +35,7 @@ Build-Depends:
python3-django-captcha,
# Explictly depend on ipware as it is optional dependecy of django-axes
python3-django-ipware,
python3-django-oauth-toolkit,
python3-django-stronghold,
python3-gi,
python3-markupsafe,
@ -56,11 +57,10 @@ Build-Depends:
sshpass,
xmlto,
xsltproc
Standards-Version: 4.6.2
Standards-Version: 4.7.3
Homepage: https://salsa.debian.org/freedombox-team/freedombox
Vcs-Git: https://salsa.debian.org/freedombox-team/freedombox.git -b debian/trixie-backports
Vcs-Browser: https://salsa.debian.org/freedombox-team/freedombox
Rules-Requires-Root: no
Package: freedombox
Breaks:
@ -89,6 +89,7 @@ Depends:
# For gdbus used to call hooks into service
libglib2.0-bin,
libjs-bootstrap5,
libjs-htmx,
lsof,
netcat-openbsd,
network-manager,
@ -108,6 +109,7 @@ Depends:
python3-django-captcha,
# Explictly depend on ipware as it is optional dependecy of django-axes
python3-django-ipware,
python3-django-oauth-toolkit,
python3-django-stronghold,
python3-gi,
python3-markupsafe,

5
debian/copyright vendored
View File

@ -2,7 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Source: https://salsa.debian.org/freedombox-team/freedombox
Files: *
Copyright: 2011-2025 FreedomBox Authors
Copyright: 2011-2026 FreedomBox Authors
License: AGPL-3+
Files: plinth/modules/jsxc/static/icons/jsxc.png
@ -79,6 +79,7 @@ Files: plinth/modules/ejabberd/static/icons/ejabberd.png
plinth/modules/rssbridge/static/icons/rssbridge.svg
plinth/modules/zoph/static/icons/zoph.png
plinth/modules/zoph/static/icons/zoph.svg
static/themes/default/img/application.svg
static/themes/default/img/network-connection.svg
static/themes/default/img/network-connection-vertical.svg
static/themes/default/img/network-ethernet.svg
@ -358,7 +359,7 @@ License: LGPL-3+ or CC-BY-SA-3.0
Files: debian/*
Copyright: 2013 Tzafrir Cohen
2013-2024 FreedomBox Authors
2013-2026 FreedomBox Authors
License: GPL-2+
License: AGPL-3+

View File

@ -24,3 +24,7 @@ freedombox: package-contains-documentation-outside-usr-share-doc [usr/lib/python
# meant for user. However, don't install to /usr/libexec and follow systemd
# convention instead.
freedombox: executable-in-usr-lib [usr/lib/freedombox/freedombox-privileged]
# [Install] section is missing for the privileged daemon service because it is
# socket activated.
freedombox: systemd-service-file-missing-install-key [usr/lib/systemd/system/freedombox-privileged.service]

View File

@ -1,43 +0,0 @@
#!/bin/sh
set -e
case "$1" in
upgrade)
# Handle removing freedombox-setup-repositories.timer from 20.5.
if dpkg --compare-versions "$2" le 20.7; then
if [ -x "/usr/bin/deb-systemd-invoke" ]; then
deb-systemd-invoke stop freedombox-setup-repositories.timer >/dev/null 2>/dev/null || true
fi
if [ -x "/usr/bin/deb-systemd-helper" ]; then
deb-systemd-helper purge freedombox-setup-repositories.timer >/dev/null || true
deb-systemd-helper unmask freedombox-setup-repositories.timer >/dev/null || true
fi
if [ -d /run/systemd/system ]; then
systemctl daemon-reload
fi
fi
# Handle removing freedombox-udiskie.service from 20.9.
if dpkg --compare-versions "$2" le 20.9; then
if [ -x "/usr/bin/deb-systemd-invoke" ]; then
deb-systemd-invoke stop freedombox-udiskie.service >/dev/null 2>/dev/null || true
fi
if [ -x "/usr/bin/deb-systemd-helper" ]; then
deb-systemd-helper purge freedombox-udiskie.service >/dev/null || true
deb-systemd-helper unmask freedombox-udiskie.service >/dev/null || true
fi
if [ -d /run/systemd/system ]; then
systemctl daemon-reload
fi
fi
;;
esac
#DEBHELPER#
exit 0

3
debian/gbp.conf vendored
View File

@ -1,6 +1,9 @@
[DEFAULT]
debian-branch = debian/trixie-backports
[buildpackage]
export-dir = ../build-area/
[dch]
git-log = --no-merges
multimaint-merge = True

View File

@ -5,7 +5,4 @@
very-long-line-length-in-source-file * [doc/manual/*.raw.wiki:*]
# Misc. files which can't be fixed to have short line lengths.
very-long-line-length-in-source-file * [plinth/modules/deluge/tests/data/sample.torrent:*]
very-long-line-length-in-source-file * [plinth/modules/transmission/tests/data/sample.torrent:*]
very-long-line-length-in-source-file * [doc/visual_design/FreedomBox-Logo.7z:*]
very-long-line-length-in-source-file * [COPYING.md:*]

View File

@ -19,6 +19,7 @@ Install the following Debian packages:
* python3-sphinx
* python3-sphinx-autobuild
* python3-sphinx-book-theme
* python3-django
* python3-django-axes
* python3-django-captcha

122
doc/dev/_static/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,15 +0,0 @@
{%- extends "alabaster/layout.html" %}
{%- block footer %}
<div class="footer">
{% if show_copyright %}&copy;{{ copyright }} | {% endif %}
Licensed under the <a href="https://creativecommons.org/licenses/by-sa/4.0/">
CC BY-SA 4.0</a> license
{%- if show_source and has_source and sourcename %}
{% if show_copyright or theme_show_powered_by %}|{% endif %}
<a href="{{ pathto('_sources/' + sourcename, true)|e }}"
rel="nofollow">{{ _('Page source') }}</a>
{%- endif %}
</div>
{% endblock %}

View File

@ -15,6 +15,7 @@ list see the documentation: http://www.sphinx-doc.org/en/master/config
#
import os
import sys
from datetime import datetime
import django
@ -26,7 +27,7 @@ django.setup()
# pylint: disable=invalid-name
project = 'FreedomBox'
copyright = '2021-2025, FreedomBox Authors'
copyright = f'2021-{datetime.now().year}'
author = 'FreedomBox Authors'
# The short X.Y version
@ -82,15 +83,23 @@ pygments_style = None
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
html_theme = 'sphinx_book_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'fixed_sidebar': True,
'show_related': True,
'home_page_in_toc': True,
'repository_provider': 'gitlab',
'repository_url': 'https://salsa.debian.org/freedombox-team/freedombox/',
'use_edit_page_button': True,
'use_source_button': True,
'use_repository_button': True,
'use_issues_button': True,
'path_to_docs': 'doc/dev/',
'extra_footer': 'Licensed under the <a href="https://creativecommons.org/'
'licenses/by-sa/4.0/">CC BY-SA 4.0</a> license.',
}
# Add any paths that contain custom static files (such as style sheets) here,
@ -221,3 +230,4 @@ autodoc_mock_imports = [
]
html_favicon = './_static/favicon.ico'
html_logo = './_static/logo.svg'

View File

@ -291,10 +291,8 @@ file ``transmission-plinth.conf``, add the following.
<Location /transmission>
...
Include includes/freedombox-single-sign-on.conf
<IfModule mod_auth_pubtkt.c>
TKTAuthToken "admin" "bit-torrent"
</IfModule>
Use AuthOpenIDConnect
Use RequireGroup bit-torrent
</Location>
Showing a shortcut in the front page

View File

@ -12,7 +12,17 @@ In addition to supporting various single board computers and other devices, any
== Recommended Hardware ==
On April 22nd, 2019, the !FreedomBox Foundation announced the [[https://freedomboxfoundation.org/buy/|sales]] of the Pioneer Edition !FreedomBox Home Server Kits. This is the recommended pre-installed hardware for all users who don't wish to build their own !FreedomBox by choosing the right components, downloading the image and preparing an SD card with !FreedomBox.
=== Libre Crafts FreedomBox ===
Libre Crafts in an endeavor from the !FreedomBox developers themselves to bring you a powerful !FreedomBox device capable of hosting even the most demanding home server needs. The device is crafted, tested, and delivered to you by !FreedomBox developers. Your purchase helps !FreedomBox development.
This hardware features a powerful CPU, plenty of main memory, a fast OS disk, ability to add two high capacity hard disk drives, dual multi-gigabit Ethernet ports, all with a low power consumption. Use it to host all your photos, to backup all home devices, as a NAS, as home automation hub, as a desktop computer, and more all at once.
||<style="text-align: center;"> [[FreedomBox/Hardware/LibreCrafts|{{attachment:libre-crafts.png|Libre Crafts FreedomBox|height=300}}]]<<BR>> [[FreedomBox/Hardware/LibreCrafts|Libre Crafts FreedomBox]] ||
=== Olimex's FreedomBox Pioneer Edition ===
On April 22nd, 2019, the !FreedomBox Foundation announced the [[https://freedomboxfoundation.org/buy/|sales]] of the Pioneer Edition !FreedomBox Home Server Kits. This pre-installed hardware is for all users who don't wish to build their own !FreedomBox by choosing the right components, downloading the image and preparing an SD card with !FreedomBox.
The kit includes all the hardware needed for launching a !FreedomBox home server on an Olimex A20-OLinuXino-LIME2 board. This product provides the perfect combination of open source hardware and free and open source software. By purchasing this product, you also support the !FreedomBox Foundation's efforts to create and promote its free and open source server software.

View File

@ -25,7 +25,8 @@ The [[https://www.olimex.com/Products/OLinuXino/Home-Server/Pioneer-FreedomBox-H
* an optional storage add-on for hard disk (HDD) or solid-state drive (SSD)
=== Recommended Hardware ===
This is the hardware recommended for all users who just want a turn-key !FreedomBox, and '''don't''' want to '''build''' their own one.
This is a hardware recommended for all users who just want a turn-key !FreedomBox, and '''don't''' want to '''build''' their own one.
(Building your own !FreedomBox means some technical stuff like choosing and buying the right components, downloading the image and preparing the SD card).

View File

@ -8,6 +8,125 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 26.4 (2026-03-02) ==
=== Highlights ===
* backups: Enable key-based SSH authentication for remote backups
* oidc: New app to implement OpenID Connect Provider
* apache: Implement protecting apps using OpenID Connect
* wireguard: Improve server section user experience flow
=== Other Changes ===
* *: Remove some absolute file paths in SVGs
* *: Update URL base from /plinth to /freedombox
* README/HACKING: Update weblate project path to /freedombox
* Vagrantfile: Drop unnecessary sudo configuration for actions
* action_utils: Drop support for link-local IPv6 addresses
* action_utils: Fix issue with type checking a generator
* action_utils: Implement utility to change umask temporarily
* actions, privileged_daemon: Drop some unused global statements
* apache: Fix diagnosing URLs protected by OpenID Connect
* apache: Preserve host header when proxying to service
* backups: Arrange form for adding remote location
* backups: Avoid some repeated text in form help text
* backups: Copy SSH client public key to remote
* backups: Create .ssh folder before creating SSH key
* backups: Create a better comment in the generated SSH key file
* backups: Display SSH public key when adding remote
* backups: Fix issue with Javascript in add remote location form
* backups: Fix showing proper error for incorrect passphrase
* backups: Fix type checking errors
* backups: Generate SSH client key if needed
* backups: Migrate to SSH key auth when mounting
* backups: Minor refactoring
* backups: Show/hide form elements instead of disabling for simplicity
* backups: Simplify handling of migration to SSH keys
* backups: Test adding/removing remote location
* backups: Tweak appearance of add remote location form
* backups: Use SSH key instead of password
* backups: Use selected SSH credential for remote
* backups: tests: Simplify functional test using more classes
* bin: Add tool to change !FreedomBox password in Django database
* calibre: Use OpenID Connect instead of pubtkt based SSO
* cfg: Drop unused actions_dir option
* cfg: Drop unused config_dir option
* container: Align terminology in printed banner
* db: Create a utility to get credentials from dbconfig
* debian: Ensure that gbp creates a clean tarball prior to build
* deluge: Use OpenID Connect instead of pubtkt based SSO
* doc/dev: Set new theme for developer documentation
* doc/dev: Use OpenID Connect instead of pubtkt based SSO
* doc/dev: always have an up-to-date copyright year
* ejabberd: Fix setting up certificates for multiple domains
* email: Use OpenID Connect instead of pubtkt based SSO
* featherwiki: Use OpenID Connect instead of pubtkt based SSO
* gitweb: Fix issue with running post init due to missing method
* gitweb: Use OpenID Connect instead of pubtkt based SSO
* js: When page load fails during install, show it to user
* letsencrypt: When copying certificate reset the umask reliably
* locale/bg: Fix several translations with HTML links (Bulgarian)
* locale/de: Fix several translations with HTML links (German)
* locale: Update translations for Albanian, Catalan, Chinese (Simplified Han script), Czech, French, German, Greek, Italian, Swedish, Tamil, Turkish
* matrixsynapse: Update apache config to proxy Synapse client API
* miniflux: Get credentials from dbconfig-common directly
* miniflux: Revert workaround for a packaging bug with DB connection
* mumble: murmurd renamed to mumble-server
* oidc: Style the page for authorizing an OIDC app
* pyproject: Use new format to specify licenses
* quassel: Explicitly set permissions on the domain configuration file
* rssbridge: Use OpenID Connect instead of pubtkt based SSO
* searx: Use OpenID Connect instead of pubtkt based SSO
* sharing: Use OpenID Connect instead of pubtkt based SSO
* sso: Merge into users module, drop pubtkt related code
* syncthing: Use OpenID Connect instead of pubtkt based SSO
* syncthing: tests: Fix tests by allowing rapid restarts
* templates: Allow building pages without navigation bar and footer
* tests: functional: Fix expecting !FreedomBox to be home page
* tests: functional: Fix reloading error page during install/uninstall
* tests: functional: Increase systemd rate limits for starting units
* tiddlywiki: Use OpenID Connect instead of pubtkt based SSO
* transmission: Use OpenID Connect instead of pubtkt based SSO
* ui: Add animation for notification dismissal
* ui: Dismiss notifications without page reload
* ui: Refactor notification delete buttons to avoid repeating code
* web_framework: Allow !FreedomBox apps to override templates
* web_server: Log requests to WSGI app
* wireguard: Accept/use netmask with IP address for server connection
* wireguard: Fix format when showing multiple endpoints of the server
* wireguard: Fix showing default route setting in server edit form
* wireguard: Fix split tunneling
* wireguard: Show status of default route in server information page
* wireguard: filter .local addresses from showClient view
* wireguard: show server vpn ip in show client page
* wordpress: Use OpenID Connect instead of pubtkt based SSO when private
== FreedomBox 26.3 (2026-02-02) ==
=== Highlights ===
* ui: Use HTMX to eliminate full page reloads
* wireguard: Add 'Start Server' button to help with client setup
=== Other Changes ===
* debian: Follows policy 4.7.3
* debian: Ignore lintian warning: service file missing Install section
* debian: Remove default Rules-Requires-Root
* debian: Remove preinst script
* debian: Update copyright years
* docs: Update container script usage
* lintian: Remove mismatched overrides
* locale: Update translations for Chinese (Simplified Han script), German, Italian, Turkish
* Makefile: Fix removing extra license file
* ui: Add HTMX as a dependency
* ui: Use HTMX to update notifications on partial page updates
* wireguard: Remove NM connections when app is uninstalled
* wireguard: Show next available client IP in Add Client form
* wireguard: Update functional tests to handle Start Server button
* wireguard: Show server endpoint on main app page
== FreedomBox 26.2 (2026-01-20) ==
* gitweb: Fix deleting last repo disables app

View File

@ -20,15 +20,13 @@ The content can be shared publicly or restricted to the users of listed allowed
=== Setting Up Shares ===
For the users to access the content through their browser it must exist and have a share. A share is an entry in the Sharing app relating:
* the Name (an thereby the URL) with which the users will ask for the content,
* the Disk Path of the content to be served and
* the sharing mode. On restricted mode, it also has the list of allowed groups.
Many shares can coexist in the same server.
Only admins can create, edit or remove shares. They'll find the Sharing app in the Apps section of !FreedomBox web interface. Sharing app is an easy to use web application with an evident interface.
Each share has its own sharing mode (public or restricted) setting. Only groups recognized by !FreedomBox service can be combined in the list of allowed groups. Groups created in the CLI won't be offered by the Sharing app.
* In !FreedomBox web interface, enable the Sharing App. Only admins can create, edit or remove shares. They'll find the Sharing app in the Apps section of the !FreedomBox web interface. Many shares can coexist in the same server.
* Add a new share
* Give it a name (an thereby the URL) with which the users will ask for the content. In the example above it would be called ''content_name''.
* The Disk Path of the content to be served. This path is relative to ''root'' on your !FreedomBox. For instance ''/var/lib/freedombox/sharing/content_name'' might be a choice.
* Sharing mode. On restricted mode, it also has the list of allowed groups. Only groups recognized by !FreedomBox service can be combined in the list of allowed groups. Groups created in the CLI won't be offered by the Sharing app.
* Create the directory specified under ''Disk Path'' on your !FreedomBox through ''Cockpit'', ''Nautilus'' or remote login.
* Make sure the user, who will provide the content, has write access to that directory for instance by making him the owner of that directory.
=== Providing/Updating Content ===

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -11,8 +11,19 @@ Además de soportar varios SBC's (single board computers) y otros dispositivos,
== Hardware Recomendado ==
El 22 de Abril de 2019, la ''!FreedomBox Foundation'' anunció que los kits ''Pioneer Edition !FreedomBox Home Server'' salían a la [[https://freedomboxfoundation.org/buy/|venta]]. Este es el hardware preinstalado recomendado para todos los usuarios que no quieran construirse su propia (máquina) !FreedomBox eligiendo los componentes adecuados, descargando la imagen y preparando una tarjeta SD con (el software) !FreedomBox.
=== Libre Crafts FreedomBox ===
Libre Crafts es una iniviativa de los propios desarrolladores de !FreedomBox para proporcionar una !FreedomBox potente capaz de alojar las necesidades más exigentes de un servidor casero.
Los propios desarrolladores de !FreedomBox la montan. prueban y entregan. Tu compra ayuda al desarrollo de !FreedomBox.
Esta máquina lleva un procesador potente, mucha memoria, CPU, un disco de sitema operativo rápido, posibilidad de añador discos duros de alta capacidad, puertos Ethernet multi-gigabit duales, todo ello con bajo consumo.
Úsalo para alojar todas tus fotos, las copias de respaldo de tus otros dispositivos, como NAS, como centro de control de domótica, como ordenador de sobremesa, y más, todo a la vez.
||<style="text-align: center;"> [[FreedomBox/Hardware/LibreCrafts|{{attachment:FreedomBox/libre-crafts.png|FreedomBox de Libre Crafts|height=300}}]]<<BR>> [[FreedomBox/Hardware/LibreCrafts|FreedomBox de Libre Crafts]] ||
=== Olimex's FreedomBox Pioneer Edition ===
On April 22nd, 2019, the !FreedomBox Foundation announced the [[https://freedomboxfoundation.org/buy/|sales]] of the Pioneer Edition !FreedomBox Home Server Kits. This pre-installed hardware is for all users who don't wish to build their own !FreedomBox by choosing the right components, downloading the image and preparing an SD card with !FreedomBox.
El kit incluye todo el hardware necesario para arrancar un servidor casero !FreedomBox sobre una placa ''Olimex A20-OLinuXino-LIME2''. Este producto proporciona la combinación perfecta de hardware de fuentes abiertas y software libre. Al comprar este producto, soportas también los esfuerzos de la ''!FreedomBox Foundation'' para crear y promover su software de servidor libre.
||<style="text-align: center;"> [[es/FreedomBox/Hardware/PioneerEdition|{{attachment:FreedomBox/Hardware/pioneer-edition_thumb.jpg|Kits de servidor doméstico FreedomBox edición Pioneer|width=320,height=257}}]]<<BR>> [[es/FreedomBox/Hardware/PioneerEdition|Kits de servidor doméstico FreedomBox edición Pioneer]] ||

View File

@ -16,7 +16,7 @@ Los servidores caseros !FreedomBox Pioneer Edition los fabrica y vende Olimex, u
== Características del Producto ==
=== HW Recomendado ===
Éste es el hardware recomendado para los usuarios que quieran simplemente una !FreedomBox llave en mano, y '''no''' quieran '''construirse''' una.
Éste es un hardware recomendado para los usuarios que quieran simplemente una !FreedomBox llave en mano, y '''no''' quieran '''construirse''' una.
(Construir tu propia !FreedomBox implica algunos tecnicismos como elegir y comprar los componentes adecuados, descargar la imágen y preparar una tarjeta SD).

View File

@ -8,6 +8,125 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 26.4 (2026-03-02) ==
=== Highlights ===
* backups: Enable key-based SSH authentication for remote backups
* oidc: New app to implement OpenID Connect Provider
* apache: Implement protecting apps using OpenID Connect
* wireguard: Improve server section user experience flow
=== Other Changes ===
* *: Remove some absolute file paths in SVGs
* *: Update URL base from /plinth to /freedombox
* README/HACKING: Update weblate project path to /freedombox
* Vagrantfile: Drop unnecessary sudo configuration for actions
* action_utils: Drop support for link-local IPv6 addresses
* action_utils: Fix issue with type checking a generator
* action_utils: Implement utility to change umask temporarily
* actions, privileged_daemon: Drop some unused global statements
* apache: Fix diagnosing URLs protected by OpenID Connect
* apache: Preserve host header when proxying to service
* backups: Arrange form for adding remote location
* backups: Avoid some repeated text in form help text
* backups: Copy SSH client public key to remote
* backups: Create .ssh folder before creating SSH key
* backups: Create a better comment in the generated SSH key file
* backups: Display SSH public key when adding remote
* backups: Fix issue with Javascript in add remote location form
* backups: Fix showing proper error for incorrect passphrase
* backups: Fix type checking errors
* backups: Generate SSH client key if needed
* backups: Migrate to SSH key auth when mounting
* backups: Minor refactoring
* backups: Show/hide form elements instead of disabling for simplicity
* backups: Simplify handling of migration to SSH keys
* backups: Test adding/removing remote location
* backups: Tweak appearance of add remote location form
* backups: Use SSH key instead of password
* backups: Use selected SSH credential for remote
* backups: tests: Simplify functional test using more classes
* bin: Add tool to change !FreedomBox password in Django database
* calibre: Use OpenID Connect instead of pubtkt based SSO
* cfg: Drop unused actions_dir option
* cfg: Drop unused config_dir option
* container: Align terminology in printed banner
* db: Create a utility to get credentials from dbconfig
* debian: Ensure that gbp creates a clean tarball prior to build
* deluge: Use OpenID Connect instead of pubtkt based SSO
* doc/dev: Set new theme for developer documentation
* doc/dev: Use OpenID Connect instead of pubtkt based SSO
* doc/dev: always have an up-to-date copyright year
* ejabberd: Fix setting up certificates for multiple domains
* email: Use OpenID Connect instead of pubtkt based SSO
* featherwiki: Use OpenID Connect instead of pubtkt based SSO
* gitweb: Fix issue with running post init due to missing method
* gitweb: Use OpenID Connect instead of pubtkt based SSO
* js: When page load fails during install, show it to user
* letsencrypt: When copying certificate reset the umask reliably
* locale/bg: Fix several translations with HTML links (Bulgarian)
* locale/de: Fix several translations with HTML links (German)
* locale: Update translations for Albanian, Catalan, Chinese (Simplified Han script), Czech, French, German, Greek, Italian, Swedish, Tamil, Turkish
* matrixsynapse: Update apache config to proxy Synapse client API
* miniflux: Get credentials from dbconfig-common directly
* miniflux: Revert workaround for a packaging bug with DB connection
* mumble: murmurd renamed to mumble-server
* oidc: Style the page for authorizing an OIDC app
* pyproject: Use new format to specify licenses
* quassel: Explicitly set permissions on the domain configuration file
* rssbridge: Use OpenID Connect instead of pubtkt based SSO
* searx: Use OpenID Connect instead of pubtkt based SSO
* sharing: Use OpenID Connect instead of pubtkt based SSO
* sso: Merge into users module, drop pubtkt related code
* syncthing: Use OpenID Connect instead of pubtkt based SSO
* syncthing: tests: Fix tests by allowing rapid restarts
* templates: Allow building pages without navigation bar and footer
* tests: functional: Fix expecting !FreedomBox to be home page
* tests: functional: Fix reloading error page during install/uninstall
* tests: functional: Increase systemd rate limits for starting units
* tiddlywiki: Use OpenID Connect instead of pubtkt based SSO
* transmission: Use OpenID Connect instead of pubtkt based SSO
* ui: Add animation for notification dismissal
* ui: Dismiss notifications without page reload
* ui: Refactor notification delete buttons to avoid repeating code
* web_framework: Allow !FreedomBox apps to override templates
* web_server: Log requests to WSGI app
* wireguard: Accept/use netmask with IP address for server connection
* wireguard: Fix format when showing multiple endpoints of the server
* wireguard: Fix showing default route setting in server edit form
* wireguard: Fix split tunneling
* wireguard: Show status of default route in server information page
* wireguard: filter .local addresses from showClient view
* wireguard: show server vpn ip in show client page
* wordpress: Use OpenID Connect instead of pubtkt based SSO when private
== FreedomBox 26.3 (2026-02-02) ==
=== Highlights ===
* ui: Use HTMX to eliminate full page reloads
* wireguard: Add 'Start Server' button to help with client setup
=== Other Changes ===
* debian: Follows policy 4.7.3
* debian: Ignore lintian warning: service file missing Install section
* debian: Remove default Rules-Requires-Root
* debian: Remove preinst script
* debian: Update copyright years
* docs: Update container script usage
* lintian: Remove mismatched overrides
* locale: Update translations for Chinese (Simplified Han script), German, Italian, Turkish
* Makefile: Fix removing extra license file
* ui: Add HTMX as a dependency
* ui: Use HTMX to update notifications on partial page updates
* wireguard: Remove NM connections when app is uninstalled
* wireguard: Show next available client IP in Add Client form
* wireguard: Update functional tests to handle Start Server button
* wireguard: Show server endpoint on main app page
== FreedomBox 26.2 (2026-01-20) ==
* gitweb: Fix deleting last repo disables app

View File

@ -19,15 +19,15 @@ El contenido se puede compartir públicamente o restringido a usuarios de una li
=== Editando comparticiones ===
Para que los usuarios accedan al contenido mediante su navegador debe existir y tener una compartición. Una compartición es una entrada en la aplicación Sharing que relaciona:
* El Nombre (y por tanto la URL) que usarán los usuarios para solicitar el contenido,
* el Ruta de acceso al contenido a servir y
* el modo de compartición. Si es restringido, también contendrá la lista de grupos autorizados.
En el mismo servidor pueden coexistir múltiples comparticiones.
Cada compartición tiene su propio ajuste de modo de compartición (pública o restringida). Sólo los grupos que reconoce el servicio !FreedomBox se pueden combinar en la lista de grupos autorizados. La aplicación ''Sharing'' no ofrecerá los grupos creados en el interfaz de línea de órdenes.
Sólo los administradores pueden crear, editar o eliminar comparticiones. Encontrarán la aplicación ''Sharing'' en la sección Aplicacions del interfaz web de !FreedomBox. La aplicación ''Sharing'' es una aplicación web fácil de usar y con un interfaz evidente.
Cada compartición tiene su priopio ajuste de modo de compartición (pública o restrigida). Sólo los grupos que reconoce el servicio !FreedomBox se pueden combinar en la lista de grupos autorizados. La aplicación ''Sharing'' no ofrecerá los grupos creados en el interfaz de línea de órdenes.
* In el interfaz web de !FreedomBox, habilita la App ''Sharing''. Sólo los administradores pueden crear, editar o eliminar comparticiones. Encontrarán la aplicación ''Sharing'' en la sección Aplicaciones del interfaz web de !FreedomBox. En el mismo servidor pueden coexistir múltiples comparticiones.
* Añadir una nueva compartición:
* Dale un nombre (y por tanto la URL) que usarán los usuarios para solicitar el contenido, En el ejemplo anterior se llamaría ''nombre del contenido''.
* La Ruta completa de acceso al contenido a servir. Por ejemplo ''/var/lib/freedombox/sharing/nombre_del_contenido''.
* El modo de compartición. Si es restringido, también contendrá la lista de grupos autorizados. Solo los grupos reconocidos por el servicio !FreedomBox se pueden combinar en la lista de grupos autorizados. La app no ofrecerá los grupos creados sólo en la línea de órdenes.
* Crea el directorio especificado en ''Ruta de Disco'' en !FreedomBox mediante ''Cockpit'', ''Nautilus'' o ingreso remoto.
* Asegúrate de que el usuario que proporcione el contenido tiene permiso para escribir en el directorio, por ejemplo, haciéndole dueño del directorio.
=== Provisionar/actualizar el contenido ===

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -73,8 +73,8 @@
<para>
This the URL fragment under which Plinth will provide its services.
Plinth is shipped with a default value of
<filename>/plinth</filename>. This means that Plinth will be
available as http://localhost:8000/plinth by default.
<filename>/freedombox</filename>. This means that Plinth will be
available as http://localhost:8000/freedombox by default.
</para>
</listitem>
</varlistentry>
@ -194,7 +194,7 @@
<synopsis>$ plinth --server_dir='/myurl'</synopsis>
<para>
Run Plinth with the '/myurl' prefix. Note that Apache forwards requests
to '/plinth' by default, so /myurl is not accessible outside of your
to '/freedombox' by default, so /myurl is not accessible outside of your
FreedomBox without adapting the apache configuration.
</para>
</example>

View File

@ -13,7 +13,7 @@ from pathlib import Path
from xml.sax.saxutils import escape
BASE_URL = 'https://wiki.debian.org/'
LOCAL_BASE = '/plinth/help/manual/{lang}/'
LOCAL_BASE = '/freedombox/help/manual/{lang}/'
ICONS_DIR = 'icons'
DEFAULT_LANGUAGE = 'en'
@ -624,21 +624,21 @@ def resolve_url(url, context):
Locally available page in default language => shortcut to local copy:
>>> resolve_url('FreedomBox/Contribute', {'language': '', 'title': ''})
'/plinth/help/manual/en/Contribute#'
'/freedombox/help/manual/en/Contribute#'
Translated available page => shortcut to local copy:
>>> resolve_url('es/FreedomBox/Contribute', {'language': '', 'title': ''})
'/plinth/help/manual/es/Contribute#'
'/freedombox/help/manual/es/Contribute#'
Available page in default language refferred as translated => shortcut to
local copy:
>>> resolve_url('en/FreedomBox/Contribute', {'language': '', 'title': ''})
'/plinth/help/manual/en/Contribute#'
'/freedombox/help/manual/en/Contribute#'
Unrecognized language => handle considering it as default language:
>>> resolve_url('missing/FreedomBox/Contribute', {'language': '', \
'title': ''})
'/plinth/help/manual/en/Contribute#'
'/freedombox/help/manual/en/Contribute#'
"""
# Process first all easy, straight forward cases:
@ -1191,11 +1191,11 @@ from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>')
[Paragraph([PlainText('a')]), Paragraph([PlainText('b ')])]
>>> parse_wiki('{{{#!wiki caution\\n\\nOnce some other app is set as the \
home page, you can only navigate to the !FreedomBox Service (Plinth) by \
typing https://myfreedombox.rocks/plinth/ into the browser. <<BR>>\\n\
typing https://myfreedombox.rocks/freedombox/ into the browser. <<BR>>\\n\
''/freedombox'' can also be used as an alias to ''/plinth''\\n}}}')
[Admonition('caution', [Paragraph([PlainText('Once some other app is set \
as the home page, you can only navigate to the FreedomBox Service (Plinth) by \
typing '), Url('https://myfreedombox.rocks/plinth/'), PlainText(' into the \
typing '), Url('https://myfreedombox.rocks/freedombox/'), PlainText(' into the \
browser. ')]), Paragraph([PlainText('/freedombox can also be used as an alias \
to /plinth ')])])]
@ -1761,7 +1761,7 @@ Features introduction</ulink>'
>>> generate_inner_docbook([Link('../../Contribute', \
[PlainText('Contribute')])], context={'title': 'FreedomBox/Manual/Hardware'})
'<ulink url="/plinth/help/manual/en/Contribute#">\
'<ulink url="/freedombox/help/manual/en/Contribute#">\
Contribute</ulink>'
>>> generate_inner_docbook([Link('/Code', \
@ -1772,9 +1772,9 @@ Code</ulink>'
>>> generate_inner_docbook([Link('DebianBug:1234', [PlainText('Bug')])])
'<ulink url="https://bugs.debian.org/1234#">Bug</ulink>'
>>> generate_inner_docbook([Link('DebianPkg:plinth', \
>>> generate_inner_docbook([Link('DebianPkg:freedombox', \
[PlainText('Plinth')])])
'<ulink url="https://packages.debian.org/plinth#">Plinth</ulink>'
'<ulink url="https://packages.debian.org/freedombox#">Plinth</ulink>'
>>> generate_inner_docbook([Link('AliothList:freedombox-discuss', \
[PlainText('Discuss')])])
@ -1911,7 +1911,7 @@ PlainText(' on it. ')])])
'<para>An alternative to downloading these images is to \
<ulink url="https://wiki.debian.org/InstallingDebianOn/TI/BeagleBone#">\
install Debian</ulink> on the BeagleBone and then \
<ulink url="/plinth/help/manual/en/Debian#">install \
<ulink url="/freedombox/help/manual/en/Debian#">install \
FreedomBox</ulink> on it. </para>'
>>> generate_inner_docbook([Paragraph([PlainText('After Roundcube is \

View File

@ -3,4 +3,4 @@
Package init file.
"""
__version__ = '26.2'
__version__ = '26.4'

View File

@ -10,6 +10,7 @@ import shutil
import subprocess
import tempfile
from contextlib import contextmanager
from typing import Generator
import augeas
@ -370,12 +371,13 @@ def get_addresses() -> list[dict[str, str | bool]]:
'url_address': hostname
})
# XXX: When a hostname is resolved to IPv6 address, it may likely
# be link-local address. Link local IPv6 addresses are valid only
# for a given link and need to be scoped with interface name such
# as '%eth0' to work. Tools such as curl don't seem to handle
# When a hostname is resolved to IPv6 address, it may likely be link-local
# address. Link local IPv6 addresses are valid only for a given link and
# need to be scoped with interface name such as '%eth0' to work. Browsers
# refused to implement support for link-local addresses (with zone IDs) in
# URLs due to platform specific parsing rules and other implementation
# difficulties. mod_auth_openidc does not support them either.
# this correctly.
# addresses.append({'kind': '6', 'address': hostname, 'numeric': False})
return addresses
@ -397,13 +399,15 @@ def get_ip_addresses() -> list[dict[str, str | bool]]:
}
if address['kind'] == '6' and address['numeric']:
if address['scope'] != 'link':
address['url_address'] = '[{0}]'.format(address['address'])
else:
address['url_address'] = '[{0}%{1}]'.format(
address['url_address'], address['interface'])
address['url_address'] = '[{0}]'.format(address['address'])
addresses.append(address)
if address['scope'] != 'link':
# Do not include link local addresses. Browsers refused to
# implement support for link-local addresses (with zone IDs) in
# URLs due to platform specific parsing rules and other
# implementation difficulties. mod_auth_openidc does not support
# them either.
addresses.append(address)
return addresses
@ -838,3 +842,13 @@ def run(command, **kwargs):
raise exception
return process
@contextmanager
def umask(mask) -> Generator:
"""Set the umask temporarily for a operation and then revert it."""
old_umask = os.umask(mask)
try:
yield
finally:
os.umask(old_umask)

View File

@ -369,7 +369,6 @@ class JSONEncoder(json.JSONEncoder):
def _setup_thread_storage():
"""Setup collection of stdout/stderr from any process in this thread."""
global thread_storage
thread_storage.stdout = b''
thread_storage.stderr = b''
@ -380,14 +379,12 @@ def _clear_thread_storage():
Python documentation is silent on whether thread local storage will be
cleaned up after a thread terminates.
"""
global thread_storage
thread_storage.stdout = None
thread_storage.stderr = None
def get_return_value_from_exception(exception):
"""Return the value to return from server when an exception is raised."""
global thread_storage
return_value = {
'result': 'exception',
'exception': {

View File

@ -12,13 +12,11 @@ logger = logging.getLogger(__name__)
# [Path] section
file_root = '/usr/share/plinth'
config_dir = '/etc/plinth'
data_dir = '/var/lib/plinth'
custom_static_dir = '/var/www/plinth/custom/static'
store_file = data_dir + '/plinth.sqlite3'
actions_dir = '/usr/share/plinth/actions'
doc_dir = '/usr/share/freedombox'
server_dir = '/plinth'
server_dir = '/freedombox'
# [Network] section
host = '127.0.0.1'
@ -111,11 +109,9 @@ def read_file(config_path):
config_items = (
('Path', 'file_root', 'string'),
('Path', 'config_dir', 'string'),
('Path', 'data_dir', 'string'),
('Path', 'custom_static_dir', 'string'),
('Path', 'store_file', 'string'),
('Path', 'actions_dir', 'string'),
('Path', 'doc_dir', 'string'),
('Path', 'server_dir', 'string'),
('Network', 'host', 'string'),

View File

@ -64,10 +64,10 @@ def fixture_load_cfg():
"""Load test configuration."""
from plinth import cfg
keys = ('file_root', 'config_dir', 'data_dir', 'custom_static_dir',
'store_file', 'actions_dir', 'doc_dir', 'server_dir', 'host',
'port', 'use_x_forwarded_for', 'use_x_forwarded_host',
'secure_proxy_ssl_header', 'box_name', 'develop')
keys = ('file_root', 'data_dir', 'custom_static_dir', 'store_file',
'doc_dir', 'server_dir', 'host', 'port', 'use_x_forwarded_for',
'use_x_forwarded_host', 'secure_proxy_ssl_header', 'box_name',
'develop')
saved_state = {}
for key in keys:
saved_state[key] = getattr(cfg, key)

40
plinth/db/dbconfig.py Normal file
View File

@ -0,0 +1,40 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Utilities for parsing dbconfig-common files with Augeas."""
import pathlib
import augeas
def get_credentials(dbconfig_path: str) -> dict[str, str]:
"""Parse dbconfig-common file with Augeas Shellvars lens."""
if not pathlib.Path(dbconfig_path).is_file():
raise FileNotFoundError(f'DB config not found: {dbconfig_path}')
aug = _load_augeas(dbconfig_path)
required = ['dbc_dbuser', 'dbc_dbpass', 'dbc_dbname']
credentials = {}
for key in required + ['dbc_dbserver']:
credentials[key] = aug.get(key).strip('\'"')
if not all(credentials.get(key) for key in required):
raise ValueError('Missing required dbconfig-common credentials')
return {
'user': credentials['dbc_dbuser'],
'password': credentials['dbc_dbpass'],
'database': credentials['dbc_dbname'],
'host': credentials['dbc_dbserver'] or 'localhost'
}
def _load_augeas(config_path: str):
"""Initialize Augeas."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
pathstr = str(config_path)
aug.transform('Shellvars', pathstr)
aug.set('/augeas/context', f'/files{pathstr}')
aug.load()
return aug

View File

@ -1,5 +1,3 @@
[Path]
file_root = %(parent_parent_dir)s
config_dir = %(file_root)s/data/etc/plinth
actions_dir = %(file_root)s/actions
doc_dir = %(file_root)s/doc

View File

@ -10,7 +10,7 @@ translating the PO file from your language directory.
Introducing yourself is important since some work may have been done
already on Debian translators discussion lists and Weblate
localization platform.
https://hosted.weblate.org/projects/freedombox/plinth/
https://hosted.weblate.org/projects/freedombox/freedombox/
https://www.debian.org/MailingLists/subscribe
## Wiki: translators landing page

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ Common Django middleware.
"""
import logging
import re
from django import urls
from django.conf import settings
@ -58,7 +59,9 @@ class SetupMiddleware(MiddlewareMixin):
except urls.Resolver404:
return
if not resolver_match.namespaces or not len(resolver_match.namespaces):
non_app_namespaces = {'oauth2_provider'}
if (not resolver_match.namespaces or not len(resolver_match.namespaces)
or (set(resolver_match.namespaces) & non_app_namespaces)):
# Requested URL does not belong to any application
return
@ -99,10 +102,17 @@ class AdminRequiredMiddleware(MiddlewareMixin):
hasattr(view_func, 'IS_NON_ADMIN'):
return
if not is_user_admin(request):
if not AdminRequiredMiddleware.check_user_group(
view_func, request):
raise PermissionDenied
public_urls = settings.STRONGHOLD_PUBLIC_URLS
if any(re.match(url, request.path_info) for url in public_urls):
return
if is_user_admin(request):
return
if AdminRequiredMiddleware.check_user_group(view_func, request):
return
raise PermissionDenied
class FirstSetupMiddleware(MiddlewareMixin):

View File

@ -1,16 +1,79 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app for Apache server."""
"""FreedomBox app for Apache server.
This module implements a mechanism for protecting various URL paths using
FreedomBox's OpenID Connect implementation as Identity Provider and
mod_auth_oidc as Relying Party. The following is a simplified description of
how the flow works:
- User accesses the URL /foo using a browser. /foo is a URL path which is
protected by this module's OpenID Connect SSO (using AuthType
openid-connect).
- mod_auth_opendic seizes control and checks for authorization. Since this is
the first visit, it starts the authentication/authorization process. It first
redirects the browser to provider discovery URL
/freedombox/apache/discover-idp/.
- This URL selects and Identity Provider based on incoming URL's host header.
It will select https://mydomain.example/freedombox/o as IDP if the original
URL is https://mydomain.example/foo. Or https://freedombox.local/freedombox/o
if the original URL is https://freedombox.local/foo. After selection it will
redirect the browser back to /apache/oidc/callback with the selected IDP in
the GET parameters.
- /apache/oidc/callback is controlled by mod_auth_openidc which receives the
IDP selection. It will then query the IDP for further information such as
authorization URL, token URL, supported scopes and claims. This is done using
a backend call to /freedombox/o/.well-known/openid-configuration.
- After determining the authorization end point (/freedombox/o/authorize/) from
the metadata, mod_auth_openidc will start the authentication/authorization
process by redirecting the browser to the URL.
- FreedomBox shows login page if the user is not already logged in. User logs
in.
- FreedomBox will show a page asking the user to authorize the application to
access information such as name and email. In case of Apache's
mod_auth_openidc, this is skipped.
- FreedomBox will redirect back to /apache/oidc/callback after various checks.
This request will contain authorization grant token and OIDC claims in
parameters.
- mod_auth_openidc connects using back channel HTTP call to token endpoint
(/freedombox/o/token/) with the authorization grant token and then obtains
access token and refresh token. OIDC claims are checked using client_secret
known only to FreedomBox IDP and mod_auth_openidc.
- The OIDC claims contains username as part of 'sub' claim. This is exported as
REMOTE_USER header. 'freedombox_groups' contains the list of groups that
FreedomBox account is part of. These, along with 'Require claim' Apache
configuration directives, are used to determine if the user should get access
to /foo path or not.
- The application providing /foo will have access to information such username
and groups as part of REMOTE_USER and other OIDC_* environment variables.
- mod_auth_openidc also sets cookies that ensure that the whole process is not
repeated when a second request for the path /foo is received.
"""
import ipaddress
import os
from django.utils.translation import gettext_lazy as _
from plinth import action_utils
from plinth import app as app_module
from plinth import cfg
from plinth.config import DropinConfigs
from plinth.daemon import Daemon, RelatedDaemon
from plinth.modules import names
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.modules.oidc.components import OpenIDConnect
from plinth.package import Packages
from plinth.signals import domain_added, domain_removed
from plinth.utils import format_lazy, is_valid_user_name
@ -23,7 +86,7 @@ class ApacheApp(app_module.App):
app_id = 'apache'
_version = 14
_version = 15
def __init__(self) -> None:
"""Create components for the app."""
@ -34,11 +97,13 @@ class ApacheApp(app_module.App):
self.add(info)
packages = Packages('packages-apache', [
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3'
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3',
'libapache2-mod-auth-openidc'
])
self.add(packages)
dropin_configs = DropinConfigs('dropin-configs-apache', [
'/etc/apache2/conf-available/10-freedombox.conf',
'/etc/apache2/conf-available/php-fpm-freedombox.conf',
'/etc/fail2ban/jail.d/apache-auth-freedombox.conf',
])
@ -59,6 +124,13 @@ class ApacheApp(app_module.App):
daemons=['apache2'], reload_daemons=True)
self.add(letsencrypt)
openidconnect = OpenIDConnect(
'openidconnect-apache', 'apache',
_('Web app protected by FreedomBox'),
redirect_uris=['https://{domain}/apache/oidc/callback'],
skip_authorization=True)
self.add(openidconnect)
daemon = Daemon('daemon-apache', 'apache2')
self.add(daemon)
@ -78,6 +150,43 @@ class ApacheApp(app_module.App):
self.enable()
def validate_host(hostname: str):
"""Check whether we are allowed to be called by a given name.
This is to prevent DNS rebinding attacks and other poor consequences in the
OpenID Connect protoctol.
"""
if hostname in ('localhost', 'ip6-localhost', 'ip6-loopback'):
return
if hostname == action_utils.get_hostname():
return
if hostname in names.components.DomainName.list_names():
return
try:
ipaddress.ip_address(hostname)
return
except ValueError:
pass
raise ValueError(f'Server not configured to be called as {hostname}')
def setup_oidc_client(netloc: str, hostname: str):
"""Setup OpenID Connect client configuration.
netloc is hostname or IP address along with port as parsed by
urllib.parse.urlparse() method from a URL.
"""
validate_host(hostname)
oidc = app_module.App.get('apache').get_component('openidconnect-apache')
privileged.setup_oidc_client(netloc, oidc.client_id,
oidc.get_client_secret())
def _on_domain_added(sender, domain_type, name='', description='',
services=None, **kwargs):
"""Add site specific configuration for a domain."""

View File

@ -301,7 +301,19 @@ def check_url(url: str, kind: str | None = None,
wrapper: str | None = None,
expected_output: str | None = None) -> bool:
"""Check whether a URL is accessible."""
command = ['curl', '--location', '-f', '-w', '%{response_code}']
# When testing a URL with cURL, following any redirections with --location.
# During those follows, store cookies that have been set and use them for
# later requests. mod_auth_openidc will set a cookie 'x_csrf' to prevent
# CSRF attacks and expect this cookie to sent back to it in later requests.
# If this cookie is not present, it will refuse to perform OIDC Discovery
# process resulting 404 errors and diagnostic failures for domains that
# have not been visited by a user recently. --cookie '' means the cURL will
# use an in-process cookie-jar for storing and retrieving cookies without
# writing to a file on the disk.
command = [
'curl', '--location', '--cookie', '', '--fail', '--write-out',
'%{response_code}'
]
if kind == '6':
# extract zone index

View File

@ -0,0 +1,28 @@
## SPDX-License-Identifier: AGPL-3.0-or-later
##
## DO NOT EDIT. If you do, FreedomBox will not automatically upgrade.
##
## Apache configuration managed by FreedomBox. If customization is needed,
## create a new configuration file with higher priority and override directives.
##
##
## Macro to protect directories or locations (potentially backed by application
## proxies) with OpenID Connect. To use the macro add 'Use AuthOpenIDConnect'.
## By default, the visitor will need to be part of the 'admin' group to be able
## to access the resource. Add additional groups using 'Use RequireGroup
## <group>'. To debug OpenID Connect related communication add 'LogLevel
## auth_openidc:debug'.
##
<Macro AuthOpenIDConnect>
<IfModule mod_auth_openidc.c>
AuthType openid-connect
Require claim freedombox_groups:admin
</IfModule>
</Macro>
<Macro RequireGroup $group>
<IfModule mod_auth_openidc.c>
Require claim freedombox_groups:$group
</IfModule>
</Macro>

View File

@ -2,12 +2,22 @@
"""Configure Apache web server."""
import glob
import json
import os
import pathlib
import re
import shutil
import urllib.parse
from plinth import action_utils
from plinth.actions import privileged
import augeas
from plinth import action_utils, utils
from plinth.actions import privileged, secret_str
openidc_config_path = pathlib.Path(
'/etc/apache2/conf-available/freedombox-openidc.conf')
metadata_dir_path = pathlib.Path(
'/var/cache/apache2/mod_auth_openidc/metadata/')
def _get_sort_key_of_version(version):
@ -70,6 +80,8 @@ def setup(old_version: int):
action_utils.run(['make-ssl-cert', 'generate-default-snakeoil'],
check=True)
_setup_oidc_config()
with action_utils.WebserverChange() as webserver:
# Disable mod_php as we have switched to mod_fcgi + php-fpm. Disable
# before switching away from mpm_prefork otherwise switching fails due
@ -114,6 +126,7 @@ def setup(old_version: int):
webserver.enable('headers', kind='module')
# Various modules for authentication/authorization
webserver.enable('auth_openidc', kind='module')
webserver.enable('authnz_ldap', kind='module')
webserver.enable('auth_pubtkt', kind='module')
@ -135,9 +148,11 @@ def setup(old_version: int):
webserver.enable('dav', kind='module')
webserver.enable('dav_fs', kind='module')
# setup freedombox site
# setup freedombox configuration
webserver.enable('10-freedombox', kind='config')
webserver.enable('freedombox', kind='config')
webserver.enable('freedombox-tls', kind='config')
webserver.enable('freedombox-openidc.conf', kind='config')
# enable serving Debian javascript libraries
webserver.enable('javascript-common', kind='config')
@ -151,6 +166,119 @@ def setup(old_version: int):
webserver.enable('freedombox-default', kind='site')
def _load_augeas():
"""Initialize augeas for this app's configuration file."""
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
aug.transform('Httpd', str(openidc_config_path))
aug.set('/augeas/context', '/files' + str(openidc_config_path))
aug.load()
return aug
def _get_mod_openidc_passphrase() -> str:
"""Read existing mod-auth-openidc passphase.
Instead of generating a new passphrase every time, use existing one. If the
passphrase changes, all the existing sessions will be logged out and users
will have login to apps again.
"""
aug = _load_augeas()
for directive in aug.match('*/directive'):
if aug.get(directive) == 'OIDCCryptoPassphrase':
return aug.get(directive + '/arg')
# Does not exist already, generate new
return utils.generate_password(size=64)
@privileged
def setup_oidc_client(netloc: str, client_id: str, client_secret: secret_str):
"""Setup client ID and secret for provided domain.
netloc is hostname or IP address along with port as parsed by
urllib.parse.urlparse() method from a URL.
"""
issuer = f'{netloc}/freedombox/o'
issuer_quoted = urllib.parse.quote_plus(issuer)
client_path = metadata_dir_path / f'{issuer_quoted}.client'
if client_path.exists():
try:
current_data = json.loads(client_path.read_text())
if (current_data['client_id'] == client_id
and current_data['client_secret'] == client_secret):
return
except Exception:
pass
client_configuration = {
'client_id': client_id,
'client_secret': client_secret
}
previous_umask = os.umask(0o077)
try:
client_path.write_text(json.dumps(client_configuration))
finally:
os.umask(previous_umask)
shutil.chown(client_path, 'www-data', 'www-data')
def _setup_oidc_config():
"""Setup Apache as a OpenID Connect Relying Party.
Ensure that auth_openidc module's metadata directory is created. It will be
used to store provider-specific configuration. Since FreedomBox will be
configured with multiple domains and some of them may not be accessible due
to the access method, we need to configure a separate IDP for each domain.
This is also because auth_openidc does not allow IDP configuration with
relative URLs.
Keep the metadata directory and configuration file unreadable by non-admin
users since they contain module's crypto secret and OIDC client secret.
"""
metadata_dir_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
metadata_dir_path.mkdir(mode=0o700, exist_ok=True)
shutil.chown(metadata_dir_path.parent, 'www-data', 'www-data')
shutil.chown(metadata_dir_path, 'www-data', 'www-data')
# XXX: Default cache type is 'shm' or shared memory. This is lost when
# Apache is restarted and users/apps will have to reauthenticate. Improve
# this by using file (in tmpfs), redis, or memache caches.
passphrase = _get_mod_openidc_passphrase()
configuration = f'''##
## OpenID Connect related configuration
##
<IfModule mod_auth_openidc.c>
OIDCCryptoPassphrase {passphrase}
OIDCMetadataDir {str(metadata_dir_path)}
# Use relative URL to redirect to the same origin as the resource
OIDCDiscoverURL /freedombox/apache/discover-idp/
OIDCSSLValidateServer Off
OIDCProviderMetadataRefreshInterval 86400
# Use relative URL to return to the original domain
OIDCRedirectURI /apache/oidc/callback
OIDCRemoteUserClaim sub
# The redirect URI must always be under a location protected by
# mod_openidc.
<Location /apache>
AuthType openid-connect
# Checking audience is not necessary, but we need to check some claim.
Require claim aud:apache
</Location>
</IfModule>
'''
previous_umask = os.umask(0o077)
try:
openidc_config_path.write_text(configuration)
finally:
os.umask(previous_umask)
# TODO: Check that the (name, kind) is a managed by FreedomBox before
# performing operation.
@privileged

View File

@ -473,7 +473,8 @@ def test_diagnose_url(get_addresses, check):
def test_check_url(run):
"""Test checking whether a URL is accessible."""
url = 'http://localhost/test'
basic_command = ['curl', '--location', '-f', '-w', '%{response_code}']
basic_command = ['curl', '--location', '--cookie', '', '--fail',
'--write-out', '%{response_code}']
extra_args = {'env': None, 'check': True, 'stdout': -1, 'stderr': -1}
# Basic

View File

@ -1,6 +1,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
URLs for the Apache module.
"""
"""URLs for the Apache module."""
urlpatterns: list = []
from django.urls import re_path
from stronghold.decorators import public
from .views import DiscoverIDPView
urlpatterns = [
re_path(r'^apache/discover-idp/$', public(DiscoverIDPView.as_view()),
name='discover-idp'),
]

View File

@ -0,0 +1,71 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Views for the Apache app."""
from urllib.parse import urlencode, urlparse
from django.http import (HttpResponseBadRequest, HttpResponseRedirect,
HttpResponseServerError)
from django.views import View
from . import setup_oidc_client, validate_host
# By default 'openid' scope already included by mod_auth_openidc
OIDC_SCOPES = 'email freedombox_groups'
class DiscoverIDPView(View):
"""A view called by auth_openidc Apache module to find the IDP.
According to documentation for auth_openidc: an Issuer selection can be
passed back to the callback URL as in:
<callback-url>?iss=[${issuer}|${domain}|${e-mail-style-account-name}]
[parameters][&login_hint=<login-hint>][&scopes=<scopes>]
[&auth_request_params=<params>]
where the <iss> parameter contains the URL-encoded issuer value of the
selected Provider (or...), [parameters] contains the additional parameters
that were passed in on the discovery request (e.g.
target_link_uri=<url>&x_csrf=<x_csrf>&method=<method>&scopes=<scopes>)
"""
def get(self, request):
"""Redirect back to auth_openidc module after selecting a IDP."""
target_link_uri = request.GET.get('target_link_uri', '')
method = request.GET.get('method', 'get')
x_csrf = request.GET.get('x_csrf', '')
oidc_callback = request.GET.get('oidc_callback')
if method != 'get':
return HttpResponseBadRequest(f'Cannot handle "{method}" method')
oidc_callback_parts = urlparse(oidc_callback)
request_host = request.META['HTTP_HOST']
if request_host != oidc_callback_parts.netloc:
return HttpResponseBadRequest(
f'Cannot redirect from {request_host} to a different host '
f'{oidc_callback_parts.netloc}')
try:
validate_host(oidc_callback_parts.hostname)
except ValueError:
return HttpResponseBadRequest(
f'Accessed using unknown domain {request_host}. Please add '
'the domain to list of configured domains.')
try:
setup_oidc_client(oidc_callback_parts.netloc,
oidc_callback_parts.hostname)
except ValueError:
return HttpResponseServerError(
f'Server not configured to called as {request_host}')
url = '/apache/oidc/callback'
params = {
'iss': f'https://{request_host}/freedombox/o',
'target_link_uri': target_link_uri,
'method': method,
'x_csrf': x_csrf,
'scopes': OIDC_SCOPES,
}
params = urlencode(params)
return HttpResponseRedirect(f'{url}?{params}')

View File

@ -1,12 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to manage backup archives."""
import collections.abc
import contextlib
import json
import logging
import os
import pathlib
import re
import subprocess
from pathlib import Path
from django.utils.text import get_valid_filename
from django.utils.translation import gettext_lazy as _
@ -14,9 +16,10 @@ from django.utils.translation import gettext_noop
from plinth import app as app_module
from plinth import cfg, glib, menu
from plinth.modules.names.components import DomainName
from plinth.package import Packages
from . import api, manifest, privileged
from . import api, errors, manifest, privileged
logger = logging.getLogger(__name__)
@ -132,9 +135,77 @@ def restore_from_upload(path, app_ids=None):
create_subvolume=False, backup_file=path)
def get_known_hosts_path():
def get_known_hosts_path() -> Path:
"""Return the path to the known hosts file."""
return pathlib.Path(cfg.data_dir) / '.ssh' / 'known_hosts'
return Path(cfg.data_dir) / '.ssh' / 'known_hosts'
def get_ssh_client_auth_key_paths() -> tuple[Path, Path]:
"""Return the paths to the SSH client public key and private key."""
key_path = Path(cfg.data_dir) / '.ssh' / 'id_ed25519'
pubkey_path = key_path.with_suffix('.pub')
return pubkey_path, key_path
def generate_ssh_client_auth_key():
"""Generate SSH client authentication keypair, if needed."""
_, key_path = get_ssh_client_auth_key_paths()
key_path.parent.mkdir(parents=True, exist_ok=True)
if key_path.exists():
logger.info('SSH client key %s for FreedomBox service already exists',
key_path)
return
domain_name = DomainName.list_names()[0]
logger.info('Generating SSH client key %s for FreedomBox service',
key_path)
subprocess.run([
'ssh-keygen', '-t', 'ed25519', '-N', '', '-C',
f'freedombox@{domain_name}', '-f',
str(key_path)
], stdout=subprocess.DEVNULL, check=True)
def get_ssh_client_public_key() -> str:
"""Get SSH client public key for FreedomBox service."""
pubkey_path, _ = get_ssh_client_auth_key_paths()
with pubkey_path.open('r') as pubkey_file:
pubkey = pubkey_file.read()
return pubkey
def copy_ssh_client_public_key(pubkey_path: str, hostname: str, username: str,
password: str):
"""Copy the SSH client public key to the remote server.
Returns whether the copy was successful, and any error message.
"""
env = os.environ.copy()
env['SSHPASS'] = str(password)
with raise_ssh_error():
try:
subprocess.run([
'sshpass', '-e', 'ssh-copy-id', '-i', pubkey_path,
f'{username}@{hostname}'
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True,
env=env)
logger.info("Copied SSH client public key to remote host's "
"authorized keys.")
except subprocess.CalledProcessError as exception:
logger.warning('Failed to copy SSH client public key: %s',
exception.stderr)
raise
@contextlib.contextmanager
def raise_ssh_error() -> collections.abc.Generator[None]:
"""Convert subprocess error to SshError."""
try:
yield
except subprocess.CalledProcessError as exception:
raise errors.SshError(exception.returncode, exception.cmd,
exception.output, exception.stderr)
def is_ssh_hostkey_verified(hostname):

View File

@ -1,18 +1,24 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import subprocess
from plinth.errors import PlinthError
class BorgError(PlinthError):
"""Generic borg errors"""
"""Generic borg errors."""
class BorgRepositoryDoesNotExistError(BorgError):
"""Borg access to a repository works but the repository does not exist"""
"""Borg access to a repository works but the repository does not exist."""
class SshError(subprocess.CalledProcessError):
"""Error when running an SSH command."""
class SshfsError(PlinthError):
"""Generic sshfs errors"""
"""Generic sshfs errors."""
class BorgRepositoryExists(BorgError):

View File

@ -184,7 +184,7 @@ class EncryptedBackupsMixin(forms.Form):
choices=[('repokey', _('Key in Repository')), ('none', _('None'))])
encryption_passphrase = forms.CharField(
label=_('Passphrase'),
help_text=_('Passphrase; Only needed when using encryption.'),
help_text=_('Only needed when using encryption.'),
widget=forms.PasswordInput(), required=False)
confirm_encryption_passphrase = forms.CharField(
label=_('Confirm Passphrase'), help_text=_('Repeat the passphrase.'),
@ -251,13 +251,30 @@ 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=_('Choose how to authenticate to the remote SSH 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 />'
'SSH key-based authentication is not yet possible.'),
widget=forms.PasswordInput(), required=False)
label=_('SSH server password'), widget=forms.PasswordInput(),
strip=True, help_text=_('Required 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):
"""Perform additional checks on form data."""
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."""

View File

@ -49,7 +49,7 @@ KNOWN_ERRORS = [
'raise_as': errors.BorgRepositoryDoesNotExistError,
},
{
'errors': ['passphrase supplied in .* is incorrect'],
'errors': ['[Pp]assphrase supplied in .* is incorrect'],
'message': _('Incorrect encryption passphrase'),
'raise_as': errors.BorgError,
},
@ -131,8 +131,7 @@ def _reraise_known_errors(err):
@reraise_known_errors
@privileged
def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
password: secret_str | None = None,
def mount(mountpoint: str, remote_path: str, ssh_keyfile: str,
user_known_hosts_file: str = '/dev/null'):
"""Mount a remote ssh path via sshfs."""
try:
@ -156,16 +155,9 @@ def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
'sshfs', remote_path, mountpoint, '-o',
f'UserKnownHostsFile={user_known_hosts_file}', '-o',
'StrictHostKeyChecking=yes', '-o', 'reconnect', '-o',
'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3'
'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3', '-o',
'IdentityFile=' + ssh_keyfile
]
if ssh_keyfile:
cmd += ['-o', 'IdentityFile=' + ssh_keyfile]
else:
if not password:
raise ValueError('mount requires either a password or ssh_keyfile')
cmd += ['-o', 'password_stdin']
input_ = password.encode()
action_utils.run(cmd, check=True, timeout=TIMEOUT, input=input_)

View File

@ -13,7 +13,9 @@ from django.utils.translation import gettext_lazy as _
from plinth import cfg
from plinth.utils import format_lazy
from . import (_backup_handler, api, errors, get_known_hosts_path, privileged,
from . import (_backup_handler, api, copy_ssh_client_public_key, errors,
generate_ssh_client_auth_key, get_known_hosts_path,
get_ssh_client_auth_key_paths, privileged, raise_ssh_error,
restore_archive_handler, split_path, store)
from .schedule import Schedule
@ -143,6 +145,9 @@ class BaseBorgRepository(abc.ABC):
privileged.delete_archive(archive_path,
self._get_encryption_passpharse())
def migrate_credentials(self) -> None:
"""Migrate any credentials."""
def initialize(self):
"""Initialize / create a borg repository."""
encryption = 'none'
@ -315,11 +320,27 @@ class SshBorgRepository(BaseBorgRepository):
self._umount_ignore_errors()
@property
def hostname(self):
def hostname(self) -> str:
"""Return hostname from the remote path."""
_, hostname, _ = split_path(self._path)
return hostname.split('%')[0] # XXX: Likely incorrect to split
@property
def username(self) -> str:
"""Return username from the remote path."""
username, _, _ = split_path(self._path)
return username
@property
def ssh_password(self) -> str | None:
"""Return SSH password if it is stored, otherwise None."""
return self.credentials.get('ssh_password')
@property
def ssh_keyfile(self) -> str | None:
"""Return path to SSH client key if stored, otherwise None."""
return self.credentials.get('ssh_keyfile')
@property
def _mountpoint(self):
"""Return the local mount point where repository is to be mounted."""
@ -330,8 +351,22 @@ class SshBorgRepository(BaseBorgRepository):
"""Return whether remote path is mounted locally."""
return privileged.is_mounted(self._mountpoint)
def migrate_credentials(self) -> None:
"""Add SSH keyfile credential and delete stored password."""
if not self.ssh_password:
return
pubkey_path, keyfile_path = get_ssh_client_auth_key_paths()
generate_ssh_client_auth_key()
copy_ssh_client_public_key(str(pubkey_path), self.hostname,
self.username, self.ssh_password)
self.credentials['ssh_keyfile'] = str(keyfile_path)
self.credentials.pop('ssh_password', None)
self.save()
def initialize(self):
"""Initialize the repository after mounting the target directory."""
self.migrate_credentials()
self._ensure_remote_directory()
self.mount()
super().initialize()
@ -341,17 +376,11 @@ class SshBorgRepository(BaseBorgRepository):
if self.is_mounted:
return
self.migrate_credentials()
known_hosts_path = get_known_hosts_path()
kwargs = {'user_known_hosts_file': str(known_hosts_path)}
if 'ssh_password' in self.credentials and self.credentials[
'ssh_password']:
kwargs['password'] = self.credentials['ssh_password']
if 'ssh_keyfile' in self.credentials and self.credentials[
'ssh_keyfile']:
kwargs['ssh_keyfile'] = self.credentials['ssh_keyfile']
privileged.mount(self._mountpoint, self._path, **kwargs)
privileged.mount(self._mountpoint, self._path,
ssh_keyfile=self.credentials['ssh_keyfile'],
user_known_hosts_file=str(known_hosts_path))
def umount(self):
"""Unmount the remote path that was mounted locally using sshfs."""
@ -391,16 +420,16 @@ class SshBorgRepository(BaseBorgRepository):
if dir_path[0] == '~':
dir_path = '.' + dir_path[1:]
password = self.credentials['ssh_password']
# Ensure remote directory exists, check contents
env = {'SSHPASS': password}
known_hosts_path = str(get_known_hosts_path())
subprocess.run([
'sshpass', '-e', 'ssh', '-o',
f'UserKnownHostsFile={known_hosts_path}', f'{username}@{hostname}',
'mkdir', '-p', dir_path
], check=True, env=env)
with raise_ssh_error():
subprocess.run([
'ssh', '-i',
str(self.ssh_keyfile), '-o',
f'UserKnownHostsFile={known_hosts_path}', '-o',
'BatchMode=yes', f'{username}@{hostname}', 'mkdir', '-p',
dir_path
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
def get_repositories():

View File

@ -0,0 +1,57 @@
// 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 (passwordAuth.checked) {
sshPasswordField.parentElement.parentElement.style.display = 'block';
} else {
sshPasswordField.parentElement.parentElement.style.display = 'none';
}
}
function handleEncryptionTypeChange() {
let display = 'none';
if (encryptionType.value === "repokey") {
display = 'block';
}
encryptionPassphraseField.parentElement.parentElement.style.display = display;
encryptionConfirmPassphraseField.parentElement.parentElement.style.display = display;
}
keyAuth.addEventListener('change', handleAuthTypeChange);
passwordAuth.addEventListener('change', handleAuthTypeChange);
encryptionType.addEventListener('change', handleEncryptionTypeChange);
handleAuthTypeChange();
handleEncryptionTypeChange();
});

View File

@ -5,15 +5,125 @@
{% 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 %}
<h3>{{ title }}</h3>
<form class="form" method="post">
<form class="form form-add-remote-repository" method="post">
{% if form.non_field_errors %}
<div class="alert alert-danger">
<a class="close" data-dismiss="alert">&times;</a>
{% for non_field_error in form.non_field_errors %}
{{ non_field_error }}
{% endfor %}
</div>
{% endif %}
{% csrf_token %}
{{ form|bootstrap }}
<div class="form-group{% if form.repository.errors %}
has-error{% endif %}">
<label class="control-label fs-4 fw-medium" for="id_repository">
{{ form.repository.label }}
</label>
<div>
<input type="text" name="repository" class="form-control"
required id="id_repository">
{% for error in form.repository.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
<p class="help-block">
{{ form.repository.help_text|safe }}
</p>
</div>
</div>
<div class="form-group{% if form.ssh_auth_type.errors %}
has-error{% endif %}">
<label class="control-label fs-4 fw-medium">
{{ form.ssh_auth_type.label }}
</label>
<p class="help-block">
{{ form.ssh_auth_type.help_text|safe }}
</p>
{% for error in form.ssh_auth_type.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
<div class="radio">
<label>
{{ form.ssh_auth_type.0.tag }}
{{ form.ssh_auth_type.0.choice_label }}
</label>
</div>
<div>
<p>
{% blocktrans trimmed %}
The following SSH client public key must be added to the
authorized keys list on the remote machine for {{ box_name }} to
be able to connect to the remote machine:
{% endblocktrans %}
</p>
<pre>{{ ssh_client_public_key }}</pre>
</div>
<div class="radio">
<label>
{{ form.ssh_auth_type.1.tag }}
{{ form.ssh_auth_type.1.choice_label }}
</label>
</div>
<p>
{% blocktrans trimmed %}
{{ box_name }} service will attempt to connect using the provided
password. If successful, then the public key will be automatically
added to the authorized keys list, so that future connections do
not need the password.
{% endblocktrans %}
</p>
{% with _=form.ssh_password|add_input_classes %}
{% include "bootstrapform/field.html" with field=form.ssh_password %}
{% endwith %}
</div>
<div class="form-group">
<div class="form-group{% if form.encryption.errors %}
has-error{% endif %}">
<label class="control-label fs-4 fw-medium" for="id_encryption">
{{ form.encryption.label }}
</label>
<div>
<select name="encryption" class="form-control" id="id_encryption">
<option value="repokey">{{ form.encryption.0.choice_label }}</option>
<option value="none">{{ form.encryption.1.choice_label }}</option>
</select>
{% for error in form.encryption.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
<p class="help-block">
{{ form.encryption.help_text|safe }}
</p>
</div>
</div>
{% with _=form.encryption_passphrase|add_input_classes %}
{% include "bootstrapform/field.html" with field=form.encryption_passphrase %}
{% endwith %}
{% with _=form.confirm_encryption_passphrase|add_input_classes %}
{% include "bootstrapform/field.html" with field=form.confirm_encryption_passphrase %}
{% endwith %}
</div>
<div class="alert alert-warning d-flex align-items-center" role="alert">
<div class="me-2">
@ -24,8 +134,8 @@
{% blocktrans trimmed %}
The credentials for this repository are stored on your {{ box_name }}.
<br />
To restore a backup on a new {{ box_name }} you need the SSH credentials
and, if chosen, the encryption passphrase.
To restore a backup on a new {{ box_name }} you need the SSH
credentials and, if chosen, the encryption passphrase.
{% endblocktrans %}
</div>
</div>

View File

@ -4,8 +4,8 @@
{% load i18n %}
<div class="table-responsive">
<table class="table" id="archives-list">
<div class="table-responsive repository" data-repository-name="{{ repository.name }}">
<table class="table" class="archives-list">
<thead>
<tr>
<th colspan="2">
@ -23,7 +23,7 @@
</span>
{% endif %}
{{ repository.name }}
<span class="repository-name">{{ repository.name }}</span>
</div>
<div class="text-end">

Some files were not shown because too many files have changed in this diff Show More