mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-06-10 11:00:22 +00:00
freedombox Debian release 21.14.1
-----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmGealUACgkQd8DHXntl CAjrrRAAl/x7JeW7tONAc4erFARzVQ4RJzfatGB1oaDOePcHOocMrU3pggUed/cJ 8NAdwrR87qwnr3iMcB3BlMjHxVGYOVJ0mL2nNx3lhOwOXU0RuN2gjnHUC3EWbXnB mSt1tLeBiaDRZuFM10fxzGLEDg5FfXOycfG65AgX2HCeARiXwhRPpX8kZpykSKPa AYnrNyIe0Ch01s3oOFkC4xO2v0IXBHon7Np5aX4MqwLheUQfifcVJDb5tUTELoMu xHunfgG2oq4XJz9JqWh61Ev53jazUTZs1k2sj5YaqT1ZYriAr1dlOW2AXmGhaask KdY9WfeBDM7mesfPW8NOu9CmosYlVf9AT04WbcmFoy/vLr0MTvtSlEhQJHYRXhI0 QInZhDyU7YJY7jWdt36lPWzmodNFxhgsPePmbIO1jHWUd+03FNI8GzUBUXao1vyd X7OnuLdJ/x95ibBlbXwF88J2tWl9ttcPOm3BRcnJkPosVoX6Lz54e30rXiy7hex3 Ck2+s7vbMXPcj0QzEO18W6cy0cK0z/LvNUblgnoJL3T806TDitJv8A8XaUe9oWZp sh0YHnpcoMHP1hK1ylzI1e2Z5SDC6SYRrF4cgtV8ZxwrgQ5grWoHgm6BCSnzwjzY bsm40ykc+hGHrgIoD3gNYgfWbHLb3U5sKW3uH1JtB3ZQJrZy1vw= =W3wk -----END PGP SIGNATURE----- gpgsig -----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmGiPF8ACgkQd8DHXntl CAgxkhAAh+4N+0Zy0DSOSdjecWkfBxwG4lQMRvhCkfBwKxH142o9hGA787b9FUv5 qjYYAmqIdy31RwhWhpQdSAP7hqhRXzI4D9F95V9/v28P21imOE6n07L95Ny3hc3k YPTFWFtWGM5N3M0xtZKl5VBiDnOkdt2GOGR/hDrSl85m95AGzHm56zq7c78eZu91 gARtNjzNjM2ltZXursCInqi8k/vWD/0nUwl/fWA0N1As14J74iDvsbGpu+e7kARd j6nlPU5pg6No8AQ048gZ6NyS3siV5BBob0VklxkSgXWcrhtL0WVrEzE5vTEWNRyr Wnyt6mavtH5F6h3m1oknmdPyRf8pP2rTrD3vUFLYNxvbYd8E8v0R/EgfmjE8oaZ3 LRAuvnjxWiNOr4Dgyd7SeIdxwkjfM4mip7R7ggzWOZbwp7RwvEMuDipomLKODuZu cqI3TZC/mNEHrV2/Jd8aoYGfR3clV7LxZYk0ZUSYnuPacTRwI+NLr0U1jjut2L4B zpnIx1DLKvporQhshWYO8yjFUh2hEl15lOAGADjoCxsIfHz5o/DOXa3O0OUUTwRe hJsU6ZwAa53Jr6rO89yR4JD71f0U7mILEiWnUrmyNHnqAuxrd91vBT1sF7neH6eZ xfBJ9i6a+aQAewdvanHpXn7RlR7aZ3SD3hD2pdWGzi/0iOJeslA= =KNl0 -----END PGP SIGNATURE----- Merge tag 'v21.14.1' into debian/bullseye-backports freedombox Debian release 21.14.1 Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
commit
ed8c9dcf10
@ -1,5 +1,8 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Configuration helper for email server.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
@ -7,50 +10,37 @@ import os
|
||||
import sys
|
||||
|
||||
import plinth.log
|
||||
from plinth.modules.email_server import audit
|
||||
|
||||
EXIT_SYNTAX = 10
|
||||
EXIT_PERM = 20
|
||||
|
||||
# Set up logging
|
||||
plinth.log.pipe_to_syslog(to_stderr='tty')
|
||||
logger = logging.getLogger(os.path.basename(__file__))
|
||||
|
||||
|
||||
def reserved_for_root(fun):
|
||||
def wrapped(*args, **kwargs):
|
||||
if os.getuid() != 0:
|
||||
logger.critical('This action is reserved for root')
|
||||
sys.exit(EXIT_PERM)
|
||||
return fun(*args, **kwargs)
|
||||
return wrapped
|
||||
logger = logging.getLogger(__file__)
|
||||
|
||||
|
||||
def main():
|
||||
if not sys.stdin.isatty():
|
||||
print('WARNING: Output will not be shown. Check syslog for logs',
|
||||
file=sys.stderr)
|
||||
"""Parse arguments."""
|
||||
plinth.log.action_init()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-i', nargs='+', dest='ipc')
|
||||
parser.add_argument('module', help='Module to trigger action in')
|
||||
parser.add_argument('action', help='Action to trigger in module')
|
||||
parser.add_argument('arguments', help='String arguments for action',
|
||||
nargs='*')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Select the first non-empty dict item
|
||||
adict = vars(parser.parse_args())
|
||||
generator = (kv for kv in adict.items() if kv[1] is not None)
|
||||
subcommand, arguments = next(generator)
|
||||
|
||||
function = globals()['subcommand_' + subcommand]
|
||||
try:
|
||||
function(*arguments)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
_log_additional_info()
|
||||
_call(args.module, args.action, args.arguments)
|
||||
except Exception as exception:
|
||||
logger.exception(exception)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@reserved_for_root
|
||||
def subcommand_ipc(module_name, action_name, *args):
|
||||
import plinth.modules.email_server.audit as audit
|
||||
def _call(module_name, action_name, arguments):
|
||||
"""Import the module and run action as superuser."""
|
||||
if os.getuid() != 0:
|
||||
logger.critical('This action is reserved for root')
|
||||
sys.exit(EXIT_PERM)
|
||||
|
||||
# We only run actions defined in the audit module
|
||||
if module_name not in audit.__all__:
|
||||
@ -58,24 +48,18 @@ def subcommand_ipc(module_name, action_name, *args):
|
||||
sys.exit(EXIT_SYNTAX)
|
||||
|
||||
module = getattr(audit, module_name)
|
||||
function = getattr(module, 'action_' + action_name, None)
|
||||
if function is None:
|
||||
try:
|
||||
action = getattr(module, 'action_' + action_name)
|
||||
except AttributeError:
|
||||
logger.critical('Bad action: %s/%r', module_name, action_name)
|
||||
sys.exit(EXIT_SYNTAX)
|
||||
|
||||
function(*args)
|
||||
for argument in arguments:
|
||||
if not isinstance(argument, str):
|
||||
logger.critical('Bad argument: %s', argument)
|
||||
sys.exit(EXIT_SYNTAX)
|
||||
|
||||
|
||||
def _log_additional_info():
|
||||
import grp
|
||||
import pwd
|
||||
resu = ','.join(pwd.getpwuid(uid).pw_name for uid in os.getresuid())
|
||||
resg = ','.join(grp.getgrgid(gid).gr_name for gid in os.getresgid())
|
||||
pyver = sys.version.replace('\n', ' ')
|
||||
logger.error('--- Additional Information ---')
|
||||
logger.error('resuid=%s, resgid=%s', resu, resg)
|
||||
logger.error('argv=%r, cwd=%r', sys.argv, os.getcwd())
|
||||
logger.error('pyver=%s (%s)', pyver, os.uname().machine)
|
||||
action(*arguments)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -29,6 +29,13 @@ def parse_arguments():
|
||||
subparsers.add_parser('dump-database', help='Dump database to file')
|
||||
subparsers.add_parser('restore-database',
|
||||
help='Restore database from file')
|
||||
subparsers.add_parser('get-domain',
|
||||
help='Get the domain set for Tiny Tiny RSS.')
|
||||
set_domain = subparsers.add_parser(
|
||||
'set-domain', help='Set the domain to be used by Tiny Tiny RSS.')
|
||||
set_domain.add_argument(
|
||||
'domain_name',
|
||||
help='The domain name that will be used by Tiny Tiny RSS.')
|
||||
|
||||
subparsers.required = True
|
||||
return parser.parse_args()
|
||||
@ -40,6 +47,29 @@ def subcommand_pre_setup(_):
|
||||
['tt-rss tt-rss/database-type string pgsql'])
|
||||
|
||||
|
||||
def subcommand_get_domain(_):
|
||||
"""Get the domain set for Tiny Tiny RSS."""
|
||||
aug = load_augeas()
|
||||
|
||||
from urllib.parse import urlparse
|
||||
for match in aug.match('/files' + CONFIG_FILE + '/define'):
|
||||
if aug.get(match) == 'SELF_URL_PATH':
|
||||
url = aug.get(match + '/value').strip("'")
|
||||
print(urlparse(url).netloc)
|
||||
|
||||
|
||||
def subcommand_set_domain(args):
|
||||
"""Set the domain to be used by Tiny Tiny RSS."""
|
||||
url = f"'https://{args.domain_name}/tt-rss/'"
|
||||
aug = load_augeas()
|
||||
|
||||
for match in aug.match('/files' + CONFIG_FILE + '/define'):
|
||||
if aug.get(match) == 'SELF_URL_PATH':
|
||||
aug.set(match + '/value', url)
|
||||
|
||||
aug.save()
|
||||
|
||||
|
||||
def subcommand_setup(_):
|
||||
"""Setup Tiny Tiny RSS configuration."""
|
||||
aug = load_augeas()
|
||||
@ -49,9 +79,7 @@ def subcommand_setup(_):
|
||||
skip_self_url_path_exists = False
|
||||
|
||||
for match in aug.match('/files' + CONFIG_FILE + '/define'):
|
||||
if aug.get(match) == 'SELF_URL_PATH':
|
||||
aug.set(match + '/value', "'http://localhost/tt-rss/'")
|
||||
elif aug.get(match) == 'PLUGINS':
|
||||
if aug.get(match) == 'PLUGINS':
|
||||
aug.set(match + '/value', "'auth_remote, note'")
|
||||
elif aug.get(match) == '_SKIP_SELF_URL_PATH_CHECKS':
|
||||
skip_self_url_path_exists = True
|
||||
|
||||
143
debian/changelog
vendored
143
debian/changelog
vendored
@ -1,3 +1,146 @@
|
||||
freedombox (21.14.1) unstable; urgency=high
|
||||
|
||||
[ Sunil Mohan Adapa ]
|
||||
* config: Add packages component to a re-add zram-tools dependency
|
||||
|
||||
-- James Valleroy <jvalleroy@mailbox.org> Wed, 24 Nov 2021 10:36:25 -0500
|
||||
|
||||
freedombox (21.14) unstable; urgency=high
|
||||
|
||||
[ Burak Yavuz ]
|
||||
* Translated using Weblate (Turkish)
|
||||
|
||||
[ Michael Breidenbach ]
|
||||
* Translated using Weblate (Swedish)
|
||||
|
||||
[ Sunil Mohan Adapa ]
|
||||
* app: Introduce separate method for post initialization operations
|
||||
* module_loader: Split app initialization into separate steps
|
||||
* avahi: Split app initialization
|
||||
* backups: Split app initialization
|
||||
* cockpit: Split app initialization
|
||||
* diagnostics: Split app initialization
|
||||
* dynamicdns: Split app initialization
|
||||
* email_server: Don't get domain name during initialization
|
||||
* config: Split app configuration
|
||||
* letencrypt: Split app initialization
|
||||
* names: Split app initialization
|
||||
* pagekite: Split app initialization
|
||||
* storage: Split app initialization
|
||||
* tor: Split app initialziation
|
||||
* upgrades: Split app initialziation
|
||||
* ejabberd: Split app initialziation
|
||||
* gitweb: Split app initialization
|
||||
* frontpage: Avoid URL reverse during Shortcut component construction
|
||||
* menu: Avoid reversing URL during Menu component construction
|
||||
* main: Drop initializing Django when listing dependencies (Closes: #999484)
|
||||
|
||||
[ Andrij Mizyk ]
|
||||
* Translated using Weblate (Ukrainian)
|
||||
|
||||
[ Joseph Nuthalapati ]
|
||||
* names: Create a generic TLS domain selection form
|
||||
* tt-rss: Allow selection of a domain name
|
||||
|
||||
[ James Valleroy ]
|
||||
* debian: Fail build if no module dependencies found
|
||||
* datetime: Avoid error when systemctl is not available
|
||||
* locale: Update translation strings
|
||||
* doc: Fetch latest manual
|
||||
|
||||
-- James Valleroy <jvalleroy@mailbox.org> Mon, 22 Nov 2021 18:45:33 -0500
|
||||
|
||||
freedombox (21.13) unstable; urgency=medium
|
||||
|
||||
[ Burak Yavuz ]
|
||||
* Translated using Weblate (Turkish)
|
||||
|
||||
[ Andrij Mizyk ]
|
||||
* Translated using Weblate (Ukrainian)
|
||||
|
||||
[ Michael Breidenbach ]
|
||||
* Translated using Weblate (Swedish)
|
||||
* Translated using Weblate (Swedish)
|
||||
|
||||
[ Joseph Nuthalapati ]
|
||||
* utils: Fix ruamel.yaml deprecation warnings
|
||||
* components: Introduce new component - Packages
|
||||
* setup: Use packages from Packages component
|
||||
* components: Add docstrings & tutorial for Packages
|
||||
|
||||
[ Sunil Mohan Adapa ]
|
||||
* email_server: Refactor the home directory page
|
||||
* email_server: Add button for setting up home directory
|
||||
* email_server: Turn home view into a simple page rather than a tab
|
||||
* email_server: Add button for managing aliases
|
||||
* email_server: Remove aliases view from tabs list
|
||||
* email_server: Add heading for manage aliases page
|
||||
* email_server: Reduce the size of headings for aliases/homedir pages
|
||||
* email_server: aliases: Add method for checking of an alias is taken
|
||||
* email_server: aliases: Using Django forms instead of custom forms
|
||||
* email_server: aliases: Drop validation already done by form
|
||||
* email_server: aliases: Move sanitizing to form
|
||||
* email_server: aliases: Drop unnecessary sanitizing
|
||||
* email_server: aliases: Drop unused sanitizing method
|
||||
* email_server: aliases: Drop unused regex
|
||||
* email_server: yapf formatting
|
||||
* email_server: aliases: Drop hash DB and use sqlite3 directly
|
||||
* email_server: aliases: Minor refactoring
|
||||
* email_server: aliases: Minor refactoring to DB schema
|
||||
* email_server: aliases: Minor refactor to list view
|
||||
* email_server: aliases: Fix showing empty alias list message
|
||||
* email_server: aliases: Refactor for simpler organization
|
||||
* email_server: tls: Drop unimplemented TLS forms/view
|
||||
* email_server: rspamd: Turn spam management link to a button
|
||||
* email_server: domains: Add button for domain management form
|
||||
* email_server: Remove tabs from the interface
|
||||
* email_server: homedir: Fix styling to not show everything as header
|
||||
* email_server: Minor refactor of license statement in templates
|
||||
* email_server: domains: Use Django forms and views
|
||||
* email_server: domains: Add validation to form
|
||||
* email_server: action: Refactor for simplicity
|
||||
* email_server: yapf formatting
|
||||
* log, email_server: Don't use syslog instead of journald
|
||||
* email_server: action: Add argument type checking for extra safety
|
||||
* email_server: Don't use user IDs when performing lookups
|
||||
* email_server: Lookup LDAP local recipients via PAM
|
||||
* email_server: dovecot: Authenticate using PAM instead of LDAP
|
||||
* email_server: dovecot: Don't deliver mail to home directory
|
||||
* email_server: Setup /var/mail, drop home setup view
|
||||
* email_server: Use rollback journal for aliases sqlite DB
|
||||
* security: Properly handle sandbox analysis of timer units
|
||||
|
||||
[ Johannes Keyser ]
|
||||
* Translated using Weblate (German)
|
||||
|
||||
[ James Valleroy ]
|
||||
* tests: Use background fixture for each test
|
||||
* bepasty: Use BaseAppTests for functional tests
|
||||
* bind: Use BaseAppTests for functional tests
|
||||
* calibre: Use BaseAppTests for functional tests
|
||||
* deluge: Use BaseAppTests for functional tests
|
||||
* ejabberd: Use BaseAppTests for functional tests
|
||||
* gitweb: Use BaseAppTests for functional tests
|
||||
* ikiwiki: Use BaseAppTests for functional tests
|
||||
* mediawiki: Use BaseAppTests for functional tests
|
||||
* mldonkey: Use BaseAppTests for functional tests
|
||||
* openvpn: Use BaseAppTests for functional tests
|
||||
* pagekite: Use BaseAppTests for functional tests
|
||||
* radicale: Use BaseAppTests for functional tests
|
||||
* samba: Use BaseAppTests for functional tests
|
||||
* shadowsocks, syncthing: Use BaseAppTests for functional tests
|
||||
* transmission: Use BaseAppTests for functional tests
|
||||
* tahoe: Use BaseAppTests for functional tests
|
||||
* tor: Use BaseAppTests for functional tests
|
||||
* tests: functional: Add diagnostics delay parameter
|
||||
* avahi: Use systemd sandboxing
|
||||
* samba: Use systemd sandboxing for smbd/nmbd
|
||||
* debian: Add python3-openssl to autopkgtest depends
|
||||
* locale: Update translation strings
|
||||
* doc: Fetch latest manual
|
||||
|
||||
-- James Valleroy <jvalleroy@mailbox.org> Mon, 08 Nov 2021 21:34:27 -0500
|
||||
|
||||
freedombox (21.12~bpo11+1) bullseye-backports; urgency=medium
|
||||
|
||||
* Rebuild for bullseye-backports.
|
||||
|
||||
2
debian/rules
vendored
2
debian/rules
vendored
@ -10,6 +10,8 @@ override_dh_auto_install-indep:
|
||||
dh_auto_install
|
||||
./run --develop --list-dependencies | sort | tr '\n' ', ' | \
|
||||
sed -e 's/^/freedombox:Depends=/' >> debian/freedombox.substvars
|
||||
# Ensure the list of dependencies is not empty.
|
||||
test -s debian/freedombox.substvars || exit 1
|
||||
|
||||
# pybuild can run pytest. However, when the top level directory is included in
|
||||
# the path (done using manage.py), it results in import problems.
|
||||
|
||||
2
debian/tests/control
vendored
2
debian/tests/control
vendored
@ -14,4 +14,4 @@ Restrictions: needs-root
|
||||
# Run unit and integration tests on installed files.
|
||||
#
|
||||
Test-Command: PYTHONPATH='/usr/lib/python3/dist-packages/plinth/' py.test-3 -p no:cacheprovider --cov=plinth --cov-report=html:debci/htmlcov --cov-report=term
|
||||
Depends: git, python3-pytest, python3-pytest-cov, python3-pytest-django, @
|
||||
Depends: git, python3-openssl, python3-pytest, python3-pytest-cov, python3-pytest-django, @
|
||||
|
||||
@ -8,6 +8,7 @@ Components
|
||||
|
||||
info
|
||||
menu
|
||||
packages
|
||||
daemon
|
||||
firewall
|
||||
webserver
|
||||
|
||||
7
doc/dev/reference/components/packages.rst
Normal file
7
doc/dev/reference/components/packages.rst
Normal file
@ -0,0 +1,7 @@
|
||||
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
|
||||
Packages
|
||||
^^^^^^^^
|
||||
|
||||
.. autoclass:: plinth.package.Packages
|
||||
:members:
|
||||
@ -112,6 +112,36 @@ the daemon. The final argument is the list of ports that this daemon listens on.
|
||||
This information is used to check if the daemon is listening on the expected
|
||||
ports when the user requests diagnostic tests on the app.
|
||||
|
||||
Package management
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Transmission server is installed through a set of packages fetched from Debian
|
||||
package repositories. The packages required for this are passed on to a
|
||||
:class:`~plinth.package.Packages` component which takes care of installing,
|
||||
upgrading and uninstalling the Debian packages. An app might require one or more
|
||||
Debian packages to be installed.
|
||||
|
||||
.. code-block:: python3
|
||||
:caption: ``__init__.py``
|
||||
|
||||
from plinth.package import Packages
|
||||
|
||||
managed_packages = ['transmission-daemon']
|
||||
|
||||
class TransmissionApp(app_module.App):
|
||||
...
|
||||
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
packages = Packages('packages-transmission', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
The first argument uniquely identifies this instance of the `Packages`
|
||||
component. Choose an appropriate unique identifier if your app has multiple
|
||||
`Packages` components. The second argument is a list of Debian packages that
|
||||
this component is responsible for.
|
||||
|
||||
Managing web server configuration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@ -10,62 +10,20 @@
|
||||
|
||||
!FreedomBox includes the ability to backup and restore data, preferences, configuration and secrets from most of the applications. The Backups feature is built using Borg backup software. Borg is a deduplicating and compressing backup program. It is designed for efficient and secure backups. This backups feature can be used to selectively backup and restore data on an app-by-app basis. Backed up data can be stored on the !FreedomBox machine itself or on a remote server. Any remote server providing SSH access can be used as a backup storage repository for !FreedomBox backups. Data stored remotely may be encrypted and in such cases remote server cannot access your decrypted data.
|
||||
|
||||
=== Notes for Specific App Backups ===
|
||||
|
||||
=== Status of Backups Feature ===
|
||||
Unless otherwise noted here, backup of an app's data will include its configuration, secrets and other data.
|
||||
|
||||
|| '''App/Feature''' || '''Support in Version''' || '''Notes''' ||
|
||||
|| Avahi || - || no backup needed ||
|
||||
|| Backups || - || no backup needed ||
|
||||
|| bepasty || 20.14 || ||
|
||||
|| Bind || 0.41 || ||
|
||||
|| calibre || 20.15 || ||
|
||||
|| Cockpit || - || no backup needed ||
|
||||
|| Coturn || 20.8 || ||
|
||||
|| Datetime || 0.41 || ||
|
||||
|| Deluge || 0.41 || does not include downloaded/seeding files ||
|
||||
|| Diagnostics || - || no backup needed ||
|
||||
|| Dynamic DNS || 0.39 || ||
|
||||
|| ejabberd || 0.39 || includes all data and configuration ||
|
||||
|| Firewall || - || no backup needed ||
|
||||
|| Gitweb || 19.19 || ||
|
||||
|| I2P || 19.6 || ||
|
||||
|| ikiwiki || 0.39 || includes all wikis/blogs and their content ||
|
||||
|| infinoted || 0.39 || includes all data and keys ||
|
||||
|| JSXC || - || no backup needed ||
|
||||
|| Let's Encrypt || 0.42 || ||
|
||||
|| Matrix Synapse || 0.39 || includes media and uploads ||
|
||||
|| !MediaWiki || 0.39 || includes wiki pages and uploaded files ||
|
||||
|| Minetest || 0.39 || ||
|
||||
|| MiniDLNA || 19.23 || ||
|
||||
|| MLDonkey || 19.0 || ||
|
||||
|| Monkeysphere || 0.42 || ||
|
||||
|| Mumble || 0.40 || ||
|
||||
|| Names || - || no backup needed ||
|
||||
|| Networks || No || No plans currently to implement backup ||
|
||||
|| OpenVPN || 0.48 || includes all user and server keys ||
|
||||
|| Pagekite || 0.40 || ||
|
||||
|| Power || - || no backup needed ||
|
||||
|| Privoxy || - || no backup needed ||
|
||||
|| Quassel || 0.40 || includes users and logs ||
|
||||
|| Radicale || 0.39 || includes calendar and cards data for all users ||
|
||||
|| Roundcube || - || no backup needed ||
|
||||
|| Samba || 19.22 || does not include the data in the shared folders ||
|
||||
|| SearX || - || no backup needed ||
|
||||
|| Secure Shell (SSH) Server || 0.41 || includes host keys ||
|
||||
|| Security || 0.41 || ||
|
||||
|| Shadowsocks || 0.40 || only secrets ||
|
||||
|| Sharing || 0.40 || does not include the data in the shared folders ||
|
||||
|| Snapshot || 0.41 || only configuration, does not include snapshot data ||
|
||||
|| Storage || - || no backup needed ||
|
||||
|| Syncthing || 0.48 || does not include data in the shared folders ||
|
||||
|| Tahoe-LAFS || 0.42 || includes all data and configuration ||
|
||||
|| Tiny Tiny RSS || 19.2 || includes database containing feeds, stories, etc. ||
|
||||
|| Tor || 0.42 || includes configuration and secrets such as onion service keys ||
|
||||
|| Transmission || 0.40 || does not include downloaded/seeding files ||
|
||||
|| Upgrades || 0.42 || ||
|
||||
|| Users || No || No plans currently to implement backup ||
|
||||
|| Wordpress || 21.8 || ||
|
||||
|| Zoph || 21.3 || ||
|
||||
|| '''App/Feature''' || '''Notes''' ||
|
||||
|| [[FreedomBox/Manual/Deluge|Deluge]] || Does not include downloaded/seeding files ||
|
||||
|| [[FreedomBox/Manual/MiniDLNA|MiniDLNA]] || Does not include the data in the shared folders ||
|
||||
|| [[FreedomBox/Manual/Networks|Networks]] || No plans currently to implement backup ||
|
||||
|| [[FreedomBox/Manual/Samba|Samba]] || Does not include the data in the shared folders ||
|
||||
|| [[FreedomBox/Manual/Sharing|Sharing]] || Does not include the data in the shared folders ||
|
||||
|| Snapshot || Only configuration, does not include snapshot data ||
|
||||
|| [[FreedomBox/Manual/Syncthing|Syncthing]] || Does not include data in the shared folders ||
|
||||
|| [[FreedomBox/Manual/Transmission|Transmission]] || Does not include downloaded/seeding files ||
|
||||
|| [[FreedomBox/Manual/Users|Users]] || Backup of user accounts is [[https://salsa.debian.org/freedombox-team/freedombox/-/issues/2051|planned]] ||
|
||||
|
||||
=== How to install and use Backups ===
|
||||
|
||||
|
||||
@ -21,9 +21,6 @@ Only users who are members of the ''calibre'' group have access to the libraries
|
||||
|
||||
You might be familiar with the e-book reader shipped with the calibre application on your desktop. The server version of calibre that's installed on your !FreedomBox has a web-based e-book reader with similar look and feel. This allows you to read your e-books from any device with a web browser.
|
||||
|
||||
'''Note on calibre versions:'''
|
||||
Please note that depending on the Debian version your !FreedomBox is running, you might be running a different major version of calibre. Debian stable (Buster) has calibre 3.x, testing and unstable have calibre 5.x. This means that some of the experimental features like the web-based e-book reader might not work very well if you're on Debian stable. This situation will improve will the Debian 11 (Bullseye) release next year. !FreedomBox doesn't ship backported packages of calibre.
|
||||
|
||||
=== Managing Libraries ===
|
||||
|
||||
After installation of calibre, a default library called "Library" will be made available. The !FreedomBox administrator can add or delete any of the libraries including the default one from the app settings in !FreedomBox web interface.
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
== MLDonkey (Peer-to-peer File Sharing) ==
|
||||
||<tablestyle="float: right;"> {{attachment:MLDonkey-icon_en_V01.png|MLDonkey icon}} ||
|
||||
|
||||
'''Available since:''' version 0.48.0
|
||||
'''Availability:''' MLDonkey is not available in either Bullseye (stable) or Bookworm (testing).
|
||||
|
||||
=== What is MLDonkey? ===
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
* !FreedomBox Service comes installed with all !FreedomBox images. You can [[FreedomBox/Download|download]] !FreedomBox images and run on any of the supported hardware. Then, to access !FreedomBox interface see [[FreedomBox/Manual/QuickStart|quick start]] instructions.
|
||||
|
||||
* If you are on a Debian box, you may install !FreedomBox Service from Debian package archive. Currently, only Buster (stable), Bullseye (testing), and Sid (unstable) are supported. To install !FreedomBox Service run:
|
||||
* If you are on a Debian box, you may install !FreedomBox Service from Debian package archive. Currently, only bullseye (stable), bookworm (testing), and sid (unstable) are supported. To install !FreedomBox Service run:
|
||||
|
||||
{{{
|
||||
$ sudo apt-get install freedombox
|
||||
|
||||
@ -139,46 +139,6 @@ file = /etc/radicale/rights
|
||||
==== Importing files ====
|
||||
If you are using a contacts file exported from another service or application, it should be copied to: /var/lib/radicale/collections/''user''/''contact file name''.vcf.
|
||||
|
||||
=== Migrating from Radicale Version 1.x to Version 2.x ===
|
||||
|
||||
During the month of February 2019, radicale in Debian testing was upgraded from version 1.x to version 2.x. Version 2.x is a better version but is incompatible with data and configuration used with 1.x. Automatic upgrade mechanism in !FreedomBox, handled by unattended-upgrades does not automatically upgrade radicale to version 2.x due to changes in configuration files. However, !FreedomBox version 19.1, which is available on February 23rd, 2019 in testing will perform data and configuration migration to radicale version 2.x. Typical users require no action, this will happen automatically.
|
||||
|
||||
If for some reason, you need to manually run `apt dist-upgrade` on your machine, then radicale will be upgraded to 2.x and then !FreedomBox will not be able to perform its upgrade (due to upstream project deciding to remove migration tools in radicale 2.x version). To avoid this situation, the following process is recommended if you wish to perform an upgrade.
|
||||
|
||||
{{{
|
||||
sudo su -
|
||||
apt hold radicale
|
||||
apt dist-upgrade
|
||||
apt unhold radicale
|
||||
}}}
|
||||
|
||||
However, if you already happen to perform an upgrade to radicale 2.x without help from !FreedomBox, you need to perform data and configuration migration yourself. Follow this procedure:
|
||||
|
||||
{{{
|
||||
sudo su -
|
||||
tar -cvzf /root/radicale_backup.tgz /var/lib/radicale/ /etc/radicale/ /etc/default/radicale
|
||||
apt install -y python-radicale
|
||||
python -m radicale --export-storage=/root/radicale-migration
|
||||
cp -dpR /root/radicale-migration/collection-root /var/lib/radicale/collections/collection-root/
|
||||
(remove this directory if it already exists. Or perhaps merge the contents.)
|
||||
chown -R radicale:radicale /var/lib/radicale/collections/collection-root/
|
||||
apt remove -y python-radicale
|
||||
if [ -f /etc/radicale/config.dpkg-dist ] ; then cp /etc/radicale/config.dpkg-dist /etc/radicale/config ; fi
|
||||
if [ -f /etc/default/radicale.dpkg-dist ] ; then cp /etc/default/radicale.dpkg-dist /etc/default/radicale ; fi
|
||||
(After FreedomBox 19.1 is available, goto FreedomBox web interface and set your preference for calendar sharing again, if it is not the default option, as it will have been lost.)
|
||||
}}}
|
||||
|
||||
Notes:
|
||||
* python-radicale is an old package from radicale 1.x version that is still available in testing. This is a hack to use the `--export-storage` feature that is responsible for data migration. This feature is not available in radicale 2.x unfortunately.
|
||||
* Files ending with .dpkg-dist will exist only if you have chosen 'Keep your currently-installed version' when prompted for configuration file override during radicale 2.x upgrade. The above process will overwrite the old configuration with new fresh configuration. No changes are necessary to the two configuration files unless you have changed the setting for sharing calendars.
|
||||
* Note that during the migration, your data is safe in /var/lib/radicale/collections directory. New data will be created and used in /var/lib/radicale/collections/collections-root/ directory.
|
||||
* The tar command takes a backup your configuration and data in /root/radicale_backup.tgz in case you do something goes wrong and you want to undo the changes.
|
||||
|
||||
=== Troubleshooting ===
|
||||
|
||||
1. If you are using !FreedomBox Pioneer Edition or installing !FreedomBox on Debian Buster, then radicale may not be usable immediately after installation. This is due to a bug which has been fixed later. To overcome the problem, upgrade !FreedomBox by clicking on 'Manual Update' from 'Updates' app. Otherwise, simply wait a day or two and let !FreedomBox upgrade itself. After that install radicale. If radicale is already installed, disable and re-enable it after the update is completed. This will fix the problem and get radicale working properly.
|
||||
|
||||
|
||||
=== External links ===
|
||||
|
||||
* Website: https://radicale.org/3.0.html
|
||||
|
||||
@ -10,6 +10,42 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
|
||||
|
||||
The following are the release notes for each !FreedomBox version.
|
||||
|
||||
== FreedomBox 21.14 (2021-11-22) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* tt-rss: Allow selection of a domain name
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* *: Split app initialization from app construction
|
||||
* app: Introduce separate method for post initialization operations
|
||||
* datetime: Avoid error when systemctl is not available
|
||||
* debian: Fail build if no module dependencies found
|
||||
* locale: Update translations for Swedish, Turkish, Ukrainian
|
||||
* main: Drop initializing Django when listing dependencies
|
||||
|
||||
== FreedomBox 21.13 (2021-11-08) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* avahi, samba: Use systemd sandboxing
|
||||
* components: Introduce new component - Packages
|
||||
* security: Properly handle sandbox analysis of timer units
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* email_server (not enabled yet):
|
||||
* Add buttons for managing aliases, domains, spam
|
||||
* Authenticate using PAM instead of LDAP
|
||||
* Delivery mail to /var/mail instead of home directory
|
||||
* Don't use user IDs when performing lookups
|
||||
* Drop hash DB and use sqlite3 directly
|
||||
* Use Django forms and views
|
||||
* locale: Update translations for German, Swedish, Turkish, Ukrainian
|
||||
* tests: Use !BaseAppTests for functional tests of most apps
|
||||
* utils: Fix ruamel.yaml deprecation warnings
|
||||
|
||||
== FreedomBox 21.12 (2021-10-25) ==
|
||||
|
||||
* locale: Update translations for Bulgarian, Czech, French, German, Turkish, Ukrainian
|
||||
|
||||
@ -19,10 +19,7 @@
|
||||
|
||||
=== Installation ===
|
||||
|
||||
On [[DebianBuster]], wireguard is available from [[Backports]]. If your sources list contains the backports stanza, you can install wireguard from the Apps section of !FreedomBox web interface.
|
||||
{{{#!wiki caution
|
||||
WireGuard cannot be installed in !FreedomBox on buster-backports yet, because a newer version of NetworkManager is required by the !FreedomBox service to complete the setup.
|
||||
}}}
|
||||
You can install wireguard from the Apps section of the !FreedomBox web interface.
|
||||
|
||||
=== Configuration - Debian Peers ===
|
||||
|
||||
|
||||
@ -11,61 +11,19 @@
|
||||
!FreedomBox incluye la posibilidad de copiar y restaurar datos, preferencias, configuración y secretos de la mayoría de las aplicaciones. La funcionalidad de ''Backups'' se resuelve con el software de ''backup'' ''Borg''. ''Borg'' es un programa de ''backup'' con deduplicación y compresión. Está diseñado para hacer ''backups'' eficientes y seguros. Esta funcionalidad de ''backups'' se puede emplear para respaldar y recuperar datos aplicación por aplicación. Las copias de respaldado se pueden almacenar en la propia máquina !FreedomBox o en un servidor remoto. Cualquier servidor remoto con acceso por SSH se puede emplear como almacenamiento para los ''backups'' de la !FreedomBox. Las copias remotas se pueden cifrar para que el servidor remoto no pueda leer los datos que alberga.
|
||||
|
||||
|
||||
=== Estados de la Funcionalidad de Backups ===
|
||||
=== Notas para respaldos específicos ===
|
||||
Salvo que aquí se diga lo contrario, el respaldo de los datos de una aplicación incluirá su configuración, secretos y otros datos.
|
||||
|
||||
|| '''App/Funcionalidad''' || '''Soporte en Versión''' || '''Notas''' ||
|
||||
|| Avahi || - || no precisa ''backup'' ||
|
||||
|| Backups || - || no precisa ''backup'' ||
|
||||
|| bepasty || 20.14 || ||
|
||||
|| Bind || 0.41 || ||
|
||||
|| calibre || 20.15 || ||
|
||||
|| Cockpit || - || no precisa ''backup'' ||
|
||||
|| Coturn || 20.8 || ||
|
||||
|| Datetime || 0.41 || ||
|
||||
|| Deluge || 0.41 || '''no''' incluye archivos descargados ni semillas ||
|
||||
|| Diagnostics || - || no precisa ''backup'' ||
|
||||
|| Dynamic DNS || 0.39 || ||
|
||||
|| ejabberd || 0.39 || incluye todos los datos y configuración ||
|
||||
|| Firewall || - || no precisa ''backup'' ||
|
||||
|| Gitweb || 19.19 || ||
|
||||
|| I2P || 19.6 || ||
|
||||
|| ikiwiki || 0.39 || incluye todos los wikis/blogs y sus contenidos ||
|
||||
|| infinoted || 0.39 || incluye todos los datos y claves ||
|
||||
|| JSXC || - || no precisa ''backup'' ||
|
||||
|| Let's Encrypt || 0.42 || ||
|
||||
|| Matrix Synapse || 0.39 || incluye media y cargas ||
|
||||
|| !MediaWiki || 0.39 || incluye páginas de wiki y archivos adjuntos ||
|
||||
|| Minetest || 0.39 || ||
|
||||
|| MiniDLNA || 19.23 || ||
|
||||
|| MLDonkey || 19.0 || ||
|
||||
|| Monkeysphere || 0.42 || ||
|
||||
|| Mumble || 0.40 || ||
|
||||
|| Names || - || no precisa ''backup'' ||
|
||||
|| Networks || No || sin planes para implementar ''backup'', de momento ||
|
||||
|| OpenVPN || 0.48 || incluye a todos los usuarios y claves de servidor ||
|
||||
|| Pagekite || 0.40 || ||
|
||||
|| Power || - || no precisa ''backup'' ||
|
||||
|| Privoxy || - || no precisa ''backup'' ||
|
||||
|| Quassel || 0.40 || incluye usuarios y registros de ejeución (''logs'') ||
|
||||
|| Radicale || 0.39 || incluye calendario y datos de tarjetas de todos los usuarios ||
|
||||
|| Roundcube || - || no precisa ''backup'' ||
|
||||
|| Samba || 19.22 || ''no''' incluye datos de las carpetas compartidas ||
|
||||
|| SearX || - || no precisa ''backup'' ||
|
||||
|| Secure Shell (SSH) Server || 0.41 || incluye las claves del servidor ||
|
||||
|| Security || 0.41 || ||
|
||||
|| Shadowsocks || 0.40 || solo secretos ||
|
||||
|| Sharing || 0.40 || '''no''' incluye datos de las carpetas compartidas ||
|
||||
|| Snapshot || 0.41 || solo configuración, '''no''' incluye datos de capturas (snapshots) ||
|
||||
|| Storage || - || no precisa ''backup'' ||
|
||||
|| Syncthing || 0.48 || '''no''' incluye datos de las carpetas compartidas ||
|
||||
|| Tahoe-LAFS || 0.42 || incluye todos los datos y configuración ||
|
||||
|| Tiny Tiny RSS || 19.2 || incluye base de datos con ''feeds'', historias, etc. ||
|
||||
|| Tor || 0.42 || includes configuración y secretos como las claves de servicios Tor Onion ||
|
||||
|| Transmission || 0.40 || '''no''' incluye archivos descargados ni semillas ||
|
||||
|| Upgrades || 0.42 || ||
|
||||
|| Users || No || sin planes para implementar ''backup'', de momento ||
|
||||
|| Wordpress || 21.8 || ||
|
||||
|| Zoph || 21.3 || ||
|
||||
|| '''App/Funcionalidad''' || '''Notas''' ||
|
||||
|| [[es/FreedomBox/Manual/Deluge|Deluge]] || No incluye archivos descargados/sembrados ||
|
||||
|| [[es/FreedomBox/Manual/MiniDLNA|MinDLNA]] || No incluye los datos en carpetas compartidas ||
|
||||
|| [[es/FreedomBox/Manual/Networks|Redes]] || Actualmente no hay planes para implementar respaldos ||
|
||||
|| [[es/FreedomBox/Manual/Samba|Samba]] || No incluye los datos en carpetas compartidas ||
|
||||
|| [[es/FreedomBox/Manual/Sharing|Sharing]] || No incluye los datos en carpetas compartidas ||
|
||||
|| Instantáneas || Solo configuración, no incluye datos de instantánea ||
|
||||
|| [[es/FreedomBox/Manual/Syncthing|Syncthing]] || No incluye los datos en carpetas compartidas ||
|
||||
|| [[es/FreedomBox/Manual/Transmission|Transmission]] || No incluye archivos descargados/sembrados ||
|
||||
|| [[es/FreedomBox/Manual/Users|Usuarios]] || El respaldo de cuentas de usuario está [[https://salsa.debian.org/freedombox-team/freedombox/-/issues/2051|planificado]] ||
|
||||
|
||||
=== Cómo instalar y usar Backups ===
|
||||
|
||||
|
||||
@ -19,9 +19,6 @@ Sólo los usuarios del grupo ''calibre'' tienen acceso a las bibliotecas. Puedes
|
||||
|
||||
Quizá ya estés familiarizado con el lector de libros para escritorio que viene con Calibre. El servidor Calibre que se instala en tu !FreedomBox viene con un lector web con aspecto similar, lo que te permite leer tus libros desde cualquier dispositivo con navegador web.
|
||||
|
||||
'''Nota acerca de las versiones de Calibre:'''
|
||||
Dependiendo de la versión de Debian sobre la que se ejecuta tu !FreedomBox is running, tendrás una versión diferente de Calibre. Debian estable (Buster) lleva Calibre 3.x, ''en pruebas'' e ''inestable'' llevan Calibre 5.x. Esto implica que algunas funcionalidades experimentales como el lector web podrían no funcionar muy bien si estás en ''estable''. Esta situación mejorará el próximo año con la publicación de Debian 11 (Bullseye). Las actualizaciones frecuentes no abarcan a Calibre.
|
||||
|
||||
=== Administrar Bibliotecas ===
|
||||
|
||||
Tras la instalación, estará disponible una biblioteca inicial "Library". El administrador de !FreedomBox puede añadir o eliminar cualquier biblioteca incluyendo la inicial desde los ajustes de la app en el interfaz web de !FreedomBox.
|
||||
|
||||
@ -10,8 +10,7 @@
|
||||
== MLDonkey (Compartir archivos entre pares) ==
|
||||
||<tablestyle="float: right;"> {{attachment:FreedomBox/Manual/MLDonkey/MLDonkey-icon_en_V01.png|icono de MLDonkey}} ||
|
||||
|
||||
'''Disponible desde:''' versión 0.48.0
|
||||
|
||||
'''Disponible desde:''' versión 0.48.0, pero ya no está disponible ni en Bullseye (estable) ni en Bookworm (en pruebas).
|
||||
=== ¿Qué es MLDonkey? ===
|
||||
|
||||
''MLDonkey'' es una aplicación libre y multiprotocolo para compartir archivos entre pares (P2P) que ejecuta un servidor ''back-end'' sobre muchas plataformas. Se puede controlar mediante algún interfaz ''front-end'', ya sea web, telnet o cualquier otro de entre una docena de programas cliente nativos.
|
||||
|
||||
@ -6,9 +6,9 @@ El servicio !FreedomBox es [[https://www.gnu.org/philosophy/|Software Libre]] ba
|
||||
|
||||
=== Uso ===
|
||||
|
||||
* El servicio !FreedomBox viene instalado en todas las imágenes de !FreedomBox. Puedes [[FreedomBox/Download|descargar]] imágenes de !FreedomBox y ejecutarlas en cualquier hardware soportado. El servicio !FreedomBox (Plinth) estará accesible visitando la URL [[http://freedombox/plinth]] o [[https://freedombox.local/plinth]].
|
||||
* El servicio !FreedomBox viene instalado en todas las imágenes de !FreedomBox. Puedes [[FreedomBox/Download|descargar]] imágenes de !FreedomBox y ejecutarlas en cualquier hardware soportado. Para acceder al interfaz de !FreedomBox consulta [[es/FreedomBox/Manual/QuickStart|Guía rápida]].
|
||||
|
||||
* Si estás en una máquina Debian puedes instalar el servicio !FreedomBox desde el archivo de paquetes de Debian. Actualmente solo se soportan Buster (estable), Bullseye (en pruebas) y Sid (inestable). Para instalar el servicio !FreedomBox ejecuta:
|
||||
* Si estás en una máquina Debian puedes instalar el servicio !FreedomBox desde el archivo de paquetes de Debian. Actualmente solo se soportan Bullseye (estable), Bookworm (en pruebas) y Sid (inestable). Para instalar el servicio !FreedomBox ejecuta:
|
||||
|
||||
{{{
|
||||
$ sudo apt-get install freedombox
|
||||
|
||||
@ -137,45 +137,6 @@ file = /etc/radicale/rights
|
||||
==== Importar archivos ====
|
||||
Si estás usando un archivo de contactos exportado desde otro servicio o aplicación hay que copiarlo a: /var/lib/radicale/collections/<usuario>/<nombre_del_archivo_de_contactos>'.vcf.
|
||||
|
||||
=== Migrar desde Radicale versión 1.x a versión 2.x ===
|
||||
|
||||
En Febrero de 2019 se actualizó Radicale en las versiones "en pruebas" (testing) de Debian desde la versión 1.x a la 2.x. La versión 2.x es mejor pero incompatible con los datos y la configuración empleados en la 1.x. El mecanismo automático de actualización de !FreedomBox que emplean las actualizaciones desatendidas no actualiza automaticamente la version 2.x de Radicale debido a cambios en los archivos de configuración. No obstante la version 19.1 de !FreedomBox, disponible en en las versiones "en pruebas" (testing) desde el 23 de Febrero de 2019, realizará la migración de los datos y la configuración a la versión 2.x de Radicale. No se requiere ninguna acción por parte de los usuarios típicos. Ocurrirá automáticamente.
|
||||
|
||||
Si por algún motivo necesitas ejecutar a mano `apt dist-upgrade` en tu máquina Radicale se actualizará a 2.x y entonces tu !FreedomBox no podrá ejecutar esta actualización (ya que el proyecto de origen decidió eliminar las herramientas de migración de la versión 2.x de Radicale). Para evitar esta situación se recomienda el siguiente procedimiento para actualizar.
|
||||
|
||||
{{{
|
||||
sudo su -
|
||||
apt hold radicale
|
||||
apt dist-upgrade
|
||||
apt unhold radicale
|
||||
}}}
|
||||
|
||||
En cualquier caso, si ya has actualizado a Radicale 2.x sin ayuda de !FreedomBox necesitas realizar la migración de los datos y la configuración por tí mismo. Sigue este procedimiento:
|
||||
|
||||
{{{
|
||||
sudo su -
|
||||
tar -cvzf /root/radicale_backup.tgz /var/lib/radicale/ /etc/radicale/ /etc/default/radicale
|
||||
apt install -y python-radicale
|
||||
python -m radicale --export-storage=/root/radicale-migration
|
||||
cp -dpR /root/radicale-migration/collection-root /var/lib/radicale/collections/collection-root/
|
||||
(elimina este directorio si ya existe. O mezcla los contenidos.)
|
||||
chown -R radicale:radicale /var/lib/radicale/collections/collection-root/
|
||||
apt remove -y python-radicale
|
||||
if [ -f /etc/radicale/config.dpkg-dist ] ; then cp /etc/radicale/config.dpkg-dist /etc/radicale/config ; fi
|
||||
if [ -f /etc/default/radicale.dpkg-dist ] ; then cp /etc/default/radicale.dpkg-dist /etc/default/radicale ; fi
|
||||
(Cuando FreedomBox 19.1 está disponble ve al interfaz web de FreedomBox y vuelve a configurar tu preferencia de compartición de calendario si no se muestra bien porque se habrá perdido durante la operación.)
|
||||
}}}
|
||||
|
||||
Notas:
|
||||
* `python-radicale` es un paquete antigüo de la versión 1.x de Radicale que sigue disponible en las versiones "en pruebas" (testing) de Debian. Esto es un ''hack'' alternativo para emplear la funcionalidad `--export-storage` que es responsable de la migración de datos. Por desgracia esta funcionalidad ya no está disponible en Radicale 2.x.
|
||||
* Los ficheros que acaban en `.dpkg-dist` solo existirán si has elegido "Conservar tu versión actualmente instalada" cuando se te preguntó durante la actualización a Radicale 2.x. El procedimiento anterior sobrescribirá la configuración antigüa con una nueva. No se necesitan cambios a los 2 ficheros de configuración salvo que hayas cambiado la preferencia de compartición de calendario.
|
||||
* Nota: Durante la migración tus datos permanecen a salvo en el directorio `/var/lib/radicale/collections`. Los datos nuevos se crearán y usarán en el directorio `/var/lib/radicale/collections/collections-root/`.
|
||||
* El comando `tar` hace una copia de seguridad de tu configuración y tus datos en `/root/radicale_backup.tgz` por si haces o algo va mal y quieres deshacer los cambios.
|
||||
|
||||
=== Resolución de Problemas ===
|
||||
|
||||
1. Si estás usando !FreedomBox Pioneer Edition o instalando !FreedomBox sobre Debian Buster Radicale podría no estar operativo inmediatamente después de la instalación. Esto se debe a un defecto ya corregido posteriormente. Para superar el problema actualiza !FreedomBox haciendo clic en 'Actualización Manual' desde la app 'Actualizaciones'. Otra opción es simplemente esperar un par de días y dejar que !FreedomBox se actualice solo. Después instala Radicale. Si Radicale ya está instalado deshabilitalo y rehabilitalo después de que se complete la actualización. Esto arreglará el problema y dejará a Radicale trabajando correctamente.
|
||||
|
||||
|
||||
=== Enlaces externos ===
|
||||
|
||||
|
||||
@ -10,6 +10,42 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
|
||||
|
||||
The following are the release notes for each !FreedomBox version.
|
||||
|
||||
== FreedomBox 21.14 (2021-11-22) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* tt-rss: Allow selection of a domain name
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* *: Split app initialization from app construction
|
||||
* app: Introduce separate method for post initialization operations
|
||||
* datetime: Avoid error when systemctl is not available
|
||||
* debian: Fail build if no module dependencies found
|
||||
* locale: Update translations for Swedish, Turkish, Ukrainian
|
||||
* main: Drop initializing Django when listing dependencies
|
||||
|
||||
== FreedomBox 21.13 (2021-11-08) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* avahi, samba: Use systemd sandboxing
|
||||
* components: Introduce new component - Packages
|
||||
* security: Properly handle sandbox analysis of timer units
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* email_server (not enabled yet):
|
||||
* Add buttons for managing aliases, domains, spam
|
||||
* Authenticate using PAM instead of LDAP
|
||||
* Delivery mail to /var/mail instead of home directory
|
||||
* Don't use user IDs when performing lookups
|
||||
* Drop hash DB and use sqlite3 directly
|
||||
* Use Django forms and views
|
||||
* locale: Update translations for German, Swedish, Turkish, Ukrainian
|
||||
* tests: Use !BaseAppTests for functional tests of most apps
|
||||
* utils: Fix ruamel.yaml deprecation warnings
|
||||
|
||||
== FreedomBox 21.12 (2021-10-25) ==
|
||||
|
||||
* locale: Update translations for Bulgarian, Czech, French, German, Turkish, Ukrainian
|
||||
|
||||
@ -16,10 +16,7 @@
|
||||
|
||||
=== Instalación ===
|
||||
|
||||
En está disponible para [[DebianBuster]] en [[Backports]]. Si tu lista de fuentes incluye a backports, Puedes instalar wireguard desde la sección Apps del interfaz web de !FreedomBox.
|
||||
{{{#!wiki caution
|
||||
WireGuard no se puede instalar aún en !FreedomBox con buster-backports porque el servicio !FreedomBox necesita una nueva versión de NetworkManager para ponerla en funcionamiento.
|
||||
}}}
|
||||
Puedes instalar !WireGuard desde la sección ''Apps'' de la interfaz de !FreedomBox.
|
||||
|
||||
=== Configuración - Debian Peers ===
|
||||
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
Package init file.
|
||||
"""
|
||||
|
||||
__version__ = '21.12'
|
||||
__version__ = '21.14.1'
|
||||
|
||||
@ -108,7 +108,8 @@ def main():
|
||||
|
||||
if arguments.list_dependencies is not False:
|
||||
log.default_level = 'ERROR'
|
||||
web_framework.init(read_only=True)
|
||||
module_loader.load_modules()
|
||||
module_loader.apps_init()
|
||||
list_dependencies(arguments.list_dependencies)
|
||||
|
||||
log.init()
|
||||
@ -126,6 +127,8 @@ def main():
|
||||
menu.init()
|
||||
|
||||
module_loader.load_modules()
|
||||
module_loader.apps_init()
|
||||
module_loader.apps_post_init()
|
||||
frontpage.add_custom_shortcuts()
|
||||
|
||||
if arguments.setup is not False:
|
||||
|
||||
@ -41,7 +41,15 @@ class App:
|
||||
_all_apps = collections.OrderedDict()
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the app object."""
|
||||
"""Build the app by adding components.
|
||||
|
||||
App may be built just for the purpose for querying. For example, when
|
||||
querying the list of package dependencies of essential apps, an app is
|
||||
minimally constructed under a read-only environment for querying from a
|
||||
specific component. So, this operation should have no side-effects such
|
||||
connecting to signals, running configuration corrections and scheduling
|
||||
operations.
|
||||
"""
|
||||
if not self.app_id:
|
||||
raise ValueError('Invalid app ID configured')
|
||||
|
||||
@ -50,6 +58,14 @@ class App:
|
||||
# Add self to global list of apps
|
||||
self._all_apps[self.app_id] = self
|
||||
|
||||
def post_init(self):
|
||||
"""Perform post initialization operations.
|
||||
|
||||
Additional initialization operations such as connecting to signals,
|
||||
running configuration corrections and scheduling operations should be
|
||||
done in this method rather than in __init__().
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get(cls, app_id):
|
||||
"""Return an app with given ID."""
|
||||
|
||||
@ -39,6 +39,24 @@ class DomainSelectionForm(forms.Form):
|
||||
'changed later.'), choices=[])
|
||||
|
||||
|
||||
class TLSDomainForm(forms.Form):
|
||||
"""Form to select a TLS domain for an app."""
|
||||
|
||||
def get_domain_choices():
|
||||
"""Double domain entries for inclusion in the choice field."""
|
||||
from plinth.modules.names import get_available_tls_domains
|
||||
return ((domain, domain) for domain in get_available_tls_domains())
|
||||
|
||||
domain = forms.ChoiceField(
|
||||
choices=get_domain_choices(),
|
||||
label=_('TLS domain'),
|
||||
help_text=_(
|
||||
'Select a domain to use TLS with. If the list is empty, please '
|
||||
'configure at least one domain with certificates.'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class LanguageSelectionFormMixin:
|
||||
"""Form mixin for selecting the user's preferred language."""
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ class Shortcut(app.FollowerComponent):
|
||||
"""
|
||||
super().__init__(component_id)
|
||||
|
||||
if not url:
|
||||
if url is None:
|
||||
url = '?selected={id}'.format(id=component_id)
|
||||
|
||||
self.name = name
|
||||
|
||||
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
@ -5,12 +5,9 @@ Setup logging for the application.
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import logging.handlers
|
||||
import sys
|
||||
import logging.config
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
|
||||
from . import cfg
|
||||
|
||||
default_level = None
|
||||
@ -63,13 +60,8 @@ class ColoredFormatter(logging.Formatter):
|
||||
return super().format(record)
|
||||
|
||||
|
||||
def init():
|
||||
"""Setup the logging framework."""
|
||||
# Remove default handlers and let the log message propagate to root logger.
|
||||
for cherrypy_logger in [cherrypy.log.error_log, cherrypy.log.access_log]:
|
||||
for handler in list(cherrypy_logger.handlers):
|
||||
cherrypy_logger.removeHandler(handler)
|
||||
|
||||
def _capture_warnings():
|
||||
"""Capture all warnings include deprecation warnings."""
|
||||
# Capture all Python warnings such as deprecation warnings
|
||||
logging.captureWarnings(True)
|
||||
|
||||
@ -80,6 +72,25 @@ def init():
|
||||
warnings.filterwarnings('default', '', ImportWarning)
|
||||
|
||||
|
||||
def action_init():
|
||||
"""Initialize logging for action scripts."""
|
||||
_capture_warnings()
|
||||
|
||||
logging.config.dictConfig(get_configuration())
|
||||
|
||||
|
||||
def init():
|
||||
"""Setup the logging framework."""
|
||||
import cherrypy
|
||||
|
||||
# Remove default handlers and let the log message propagate to root logger.
|
||||
for cherrypy_logger in [cherrypy.log.error_log, cherrypy.log.access_log]:
|
||||
for handler in list(cherrypy_logger.handlers):
|
||||
cherrypy_logger.removeHandler(handler)
|
||||
|
||||
_capture_warnings()
|
||||
|
||||
|
||||
def setup_cherrypy_static_directory(app):
|
||||
"""Hush output from cherrypy static file request logging.
|
||||
|
||||
@ -129,24 +140,3 @@ def get_configuration():
|
||||
configuration['root']['handlers'].append('journal')
|
||||
|
||||
return configuration
|
||||
|
||||
|
||||
def pipe_to_syslog(level=logging.INFO, to_stderr=True):
|
||||
"""Make the root logger write to syslog and stderr. Useful in actions"""
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(level)
|
||||
|
||||
fmt = '/freedombox/%(name)s[%(process)d]: %(levelname)s: %(message)s'
|
||||
formatter = logging.Formatter(fmt=fmt)
|
||||
|
||||
# Using syslog in Python: https://stackoverflow.com/q/3968669
|
||||
syslog_handler = logging.handlers.SysLogHandler(address='/dev/log')
|
||||
syslog_handler.setFormatter(formatter)
|
||||
logger.addHandler(syslog_handler)
|
||||
|
||||
if to_stderr == 'tty' and sys.stdin.isatty():
|
||||
to_stderr = True
|
||||
if to_stderr is True:
|
||||
stderr_handler = logging.StreamHandler()
|
||||
stderr_handler.setFormatter(formatter)
|
||||
logger.addHandler(stderr_handler)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from plinth import app
|
||||
|
||||
@ -8,7 +8,7 @@ from plinth import app
|
||||
class Menu(app.FollowerComponent):
|
||||
"""Component to manage a single menu item."""
|
||||
|
||||
_all_menus = {}
|
||||
_all_menus = set()
|
||||
|
||||
def __init__(self, component_id, name=None, short_description=None,
|
||||
icon=None, url_name=None, url_args=None, url_kwargs=None,
|
||||
@ -57,21 +57,22 @@ class Menu(app.FollowerComponent):
|
||||
self.url = url
|
||||
self.order = order
|
||||
self.advanced = advanced
|
||||
self.items = []
|
||||
|
||||
# Add self to parent menu item
|
||||
if parent_url_name:
|
||||
parent_menu = self.get(parent_url_name)
|
||||
parent_menu.items.append(self)
|
||||
self.url_name = url_name
|
||||
self.url_args = url_args
|
||||
self.url_kwargs = url_kwargs
|
||||
self.parent_url_name = parent_url_name
|
||||
|
||||
# Add self to global list of menu items
|
||||
self._all_menus[url] = self
|
||||
# Add self to global list of menu items.
|
||||
self._all_menus.add(self)
|
||||
|
||||
@classmethod
|
||||
def get(cls, urlname, url_args=None, url_kwargs=None):
|
||||
"""Return a menu item with given URL name."""
|
||||
url = reverse(urlname, args=url_args, kwargs=url_kwargs)
|
||||
return cls._all_menus[url]
|
||||
@property
|
||||
def items(self):
|
||||
"""Return the list of children for this menu item."""
|
||||
return [
|
||||
item for item in self._all_menus
|
||||
if item.parent_url_name == self.url_name
|
||||
]
|
||||
|
||||
def sorted_items(self):
|
||||
"""Return menu items in sorted order according to current locale."""
|
||||
|
||||
@ -66,15 +66,9 @@ def load_modules():
|
||||
except KeyError:
|
||||
logger.error('Unsatified dependency for module - %s', module_name)
|
||||
|
||||
logger.info('Initializing apps - %s', ', '.join(ordered_modules))
|
||||
|
||||
for module_name in ordered_modules:
|
||||
_initialize_module(module_name, modules[module_name])
|
||||
loaded_modules[module_name] = modules[module_name]
|
||||
|
||||
logger.debug('App initialization completed.')
|
||||
post_module_loading.send_robust(sender="module_loader")
|
||||
|
||||
|
||||
def _insert_modules(module_name, module, remaining_modules, ordered_modules):
|
||||
"""Insert modules into a list based on dependency order"""
|
||||
@ -118,6 +112,13 @@ def _include_module_urls(module_import_path, module_name):
|
||||
raise
|
||||
|
||||
|
||||
def apps_init():
|
||||
"""Create apps by constructing them with components."""
|
||||
logger.info('Initializing apps - %s', ', '.join(loaded_modules))
|
||||
for module_name, module in loaded_modules.items():
|
||||
_initialize_module(module_name, module)
|
||||
|
||||
|
||||
def _initialize_module(module_name, module):
|
||||
"""Perform module initialization"""
|
||||
|
||||
@ -131,10 +132,6 @@ def _initialize_module(module_name, module):
|
||||
]
|
||||
if module_classes and app_class:
|
||||
module.app = app_class[0][1]()
|
||||
|
||||
if module.setup_helper.get_state(
|
||||
) != 'needs-setup' and module.app.is_enabled():
|
||||
module.app.set_enabled(True)
|
||||
except Exception as exception:
|
||||
logger.exception('Exception while running init for %s: %s', module,
|
||||
exception)
|
||||
@ -142,6 +139,27 @@ def _initialize_module(module_name, module):
|
||||
raise
|
||||
|
||||
|
||||
def apps_post_init():
|
||||
"""Run post initialization on each app."""
|
||||
for module in loaded_modules.values():
|
||||
if not hasattr(module, 'app') or not module.app:
|
||||
continue
|
||||
|
||||
try:
|
||||
module.app.post_init()
|
||||
if module.setup_helper.get_state(
|
||||
) != 'needs-setup' and module.app.is_enabled():
|
||||
module.app.set_enabled(True)
|
||||
except Exception as exception:
|
||||
logger.exception('Exception while running post init for %s: %s',
|
||||
module, exception)
|
||||
if cfg.develop:
|
||||
raise
|
||||
|
||||
logger.debug('App initialization completed.')
|
||||
post_module_loading.send_robust(sender="module_loader")
|
||||
|
||||
|
||||
def get_modules_to_load():
|
||||
"""Get the list of modules to be loaded"""
|
||||
global _modules_to_load
|
||||
|
||||
@ -12,6 +12,7 @@ from plinth import cfg
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.letsencrypt.components import LetsEncrypt
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy, is_valid_user_name
|
||||
|
||||
version = 9
|
||||
@ -41,6 +42,9 @@ class ApacheApp(app_module.App):
|
||||
name=_('Apache HTTP Server'))
|
||||
self.add(info)
|
||||
|
||||
packages = Packages('packages-apache', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
web_server_ports = Firewall('firewall-web', _('Web Server'),
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(web_server_ports)
|
||||
|
||||
@ -13,6 +13,7 @@ from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.config import get_hostname
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.names.components import DomainType
|
||||
from plinth.package import Packages
|
||||
from plinth.signals import domain_added, domain_removed, post_hostname_change
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
@ -65,6 +66,9 @@ class AvahiApp(app_module.App):
|
||||
'avahi:index', parent_url_name='system')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-avahi', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
domain_type = DomainType('domain-type-local',
|
||||
_('Local Network Domain'), 'config:index',
|
||||
can_have_certificate=False)
|
||||
@ -81,6 +85,8 @@ class AvahiApp(app_module.App):
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
def post_init(self):
|
||||
"""Perform post initialization operations."""
|
||||
if self.is_enabled():
|
||||
domain_added.send_robust(sender='avahi',
|
||||
domain_type='domain-type-local',
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
[Service]
|
||||
LockPersonality=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateMounts=yes
|
||||
PrivateTmp=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectHome=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectSystem=full
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
RestrictRealtime=yes
|
||||
SystemCallArchitectures=native
|
||||
@ -17,6 +17,7 @@ from django.utils.translation import gettext_noop
|
||||
from plinth import actions
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, glib, menu
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import api
|
||||
|
||||
@ -49,6 +50,7 @@ class BackupsApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(
|
||||
app_id=self.app_id, version=version, depends=depends,
|
||||
name=_('Backups'), icon='fa-files-o', description=_description,
|
||||
@ -60,6 +62,12 @@ class BackupsApp(app_module.App):
|
||||
'backups:index', parent_url_name='system')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-backups', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
@staticmethod
|
||||
def post_init():
|
||||
"""Perform post initialization operations."""
|
||||
# Check every hour (every 3 minutes in debug mode) to perform scheduled
|
||||
# backups.
|
||||
interval = 180 if cfg.develop else 3600
|
||||
|
||||
@ -13,6 +13,7 @@ from plinth import frontpage, menu
|
||||
from plinth.modules.apache.components import Uwsgi, Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import manifest
|
||||
|
||||
@ -79,6 +80,9 @@ class BepastyApp(app_module.App):
|
||||
clients=manifest.clients)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-bepasty', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-bepasty', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
@ -10,68 +10,49 @@ from plinth.tests import functional
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.bepasty]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
"""Login and install the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'bepasty')
|
||||
yield
|
||||
functional.app_disable(session_browser, 'bepasty')
|
||||
class TestBepastyApp(functional.BaseAppTests):
|
||||
app_name = 'bepasty'
|
||||
has_service = False
|
||||
has_web = True
|
||||
|
||||
def test_set_default_permissions_list_and_read_all(self, session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
_logout(session_browser)
|
||||
_set_default_permissions(session_browser, 'read list')
|
||||
|
||||
def test_enable_disable(session_browser):
|
||||
"""Test enabling the app."""
|
||||
functional.app_disable(session_browser, 'bepasty')
|
||||
assert _can_list_all(session_browser)
|
||||
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
assert functional.is_available(session_browser, 'bepasty')
|
||||
def test_set_default_permissions_read_files(self, session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
_logout(session_browser)
|
||||
_set_default_permissions(session_browser, 'read')
|
||||
|
||||
functional.app_disable(session_browser, 'bepasty')
|
||||
assert not functional.is_available(session_browser, 'bepasty')
|
||||
assert _cannot_list_all(session_browser)
|
||||
|
||||
def test_add_password(self, session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
password_added = _add_and_save_password(session_browser)
|
||||
|
||||
def test_set_default_permissions_list_and_read_all(session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
_logout(session_browser)
|
||||
_set_default_permissions(session_browser, 'read list')
|
||||
assert _can_login(session_browser, password_added)
|
||||
|
||||
assert _can_list_all(session_browser)
|
||||
def test_remove_password(self, session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
password_added = _add_and_save_password(session_browser)
|
||||
_remove_all_passwords(session_browser)
|
||||
|
||||
assert not _can_login(session_browser, password_added)
|
||||
|
||||
def test_set_default_permissions_read_files(session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
_logout(session_browser)
|
||||
_set_default_permissions(session_browser, 'read')
|
||||
@pytest.mark.backups
|
||||
def test_backup_and_restore(self, session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
password_added = _add_and_save_password(session_browser)
|
||||
functional.backup_create(session_browser, 'bepasty', 'test_bepasty')
|
||||
|
||||
assert _cannot_list_all(session_browser)
|
||||
_remove_all_passwords(session_browser)
|
||||
functional.backup_restore(session_browser, 'bepasty', 'test_bepasty')
|
||||
|
||||
|
||||
def test_add_password(session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
password_added = _add_and_save_password(session_browser)
|
||||
|
||||
assert _can_login(session_browser, password_added)
|
||||
|
||||
|
||||
def test_remove_password(session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
password_added = _add_and_save_password(session_browser)
|
||||
_remove_all_passwords(session_browser)
|
||||
|
||||
assert not _can_login(session_browser, password_added)
|
||||
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup_and_restore(session_browser):
|
||||
functional.app_enable(session_browser, 'bepasty')
|
||||
password_added = _add_and_save_password(session_browser)
|
||||
functional.backup_create(session_browser, 'bepasty', 'test_bepasty')
|
||||
|
||||
_remove_all_passwords(session_browser)
|
||||
functional.backup_restore(session_browser, 'bepasty', 'test_bepasty')
|
||||
|
||||
assert functional.is_available(session_browser, 'bepasty')
|
||||
assert _can_login(session_browser, password_added)
|
||||
assert functional.is_available(session_browser, 'bepasty')
|
||||
assert _can_login(session_browser, password_added)
|
||||
|
||||
|
||||
def _add_and_save_password(session_browser):
|
||||
|
||||
@ -16,6 +16,7 @@ from plinth import cfg, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import manifest
|
||||
@ -74,6 +75,7 @@ class BindApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
name=_('BIND'), icon='fa-globe-w',
|
||||
short_description=_('Domain Name Server'),
|
||||
@ -85,6 +87,9 @@ class BindApp(app_module.App):
|
||||
parent_url_name='system')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-bind', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-bind', info.name, ports=['dns'],
|
||||
is_external=False)
|
||||
self.add(firewall)
|
||||
|
||||
@ -4,66 +4,50 @@ Functional, browser based tests for bind app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.tests import functional
|
||||
|
||||
pytestmark = [pytest.mark.system, pytest.mark.bind]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
"""Login and install the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'bind')
|
||||
yield
|
||||
functional.app_disable(session_browser, 'bind')
|
||||
class TestBindApp(functional.BaseAppTests):
|
||||
app_name = 'bind'
|
||||
has_service = True
|
||||
has_web = False
|
||||
|
||||
def test_set_forwarders(self, session_browser):
|
||||
"""Test setting forwarders."""
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
functional.set_forwarders(session_browser, '1.1.1.1')
|
||||
|
||||
def test_enable_disable(session_browser):
|
||||
"""Test enabling the app."""
|
||||
functional.app_disable(session_browser, 'bind')
|
||||
functional.set_forwarders(session_browser, '1.1.1.1 1.0.0.1')
|
||||
assert functional.get_forwarders(session_browser) == '1.1.1.1 1.0.0.1'
|
||||
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
assert functional.service_is_running(session_browser, 'bind')
|
||||
def test_enable_disable_dnssec(self, session_browser):
|
||||
"""Test enabling/disabling DNSSEC."""
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
_enable_dnssec(session_browser, False)
|
||||
|
||||
functional.app_disable(session_browser, 'bind')
|
||||
assert functional.service_is_not_running(session_browser, 'bind')
|
||||
_enable_dnssec(session_browser, True)
|
||||
assert _get_dnssec(session_browser)
|
||||
|
||||
_enable_dnssec(session_browser, False)
|
||||
assert not _get_dnssec(session_browser)
|
||||
|
||||
def test_set_forwarders(session_browser):
|
||||
"""Test setting forwarders."""
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
functional.set_forwarders(session_browser, '1.1.1.1')
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(self, session_browser):
|
||||
"""Test backup and restore."""
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
functional.set_forwarders(session_browser, '1.1.1.1')
|
||||
_enable_dnssec(session_browser, False)
|
||||
functional.backup_create(session_browser, 'bind', 'test_bind')
|
||||
|
||||
functional.set_forwarders(session_browser, '1.1.1.1 1.0.0.1')
|
||||
assert functional.get_forwarders(session_browser) == '1.1.1.1 1.0.0.1'
|
||||
functional.set_forwarders(session_browser, '1.0.0.1')
|
||||
_enable_dnssec(session_browser, True)
|
||||
|
||||
|
||||
def test_enable_disable_dnssec(session_browser):
|
||||
"""Test enabling/disabling DNSSEC."""
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
_enable_dnssec(session_browser, False)
|
||||
|
||||
_enable_dnssec(session_browser, True)
|
||||
assert _get_dnssec(session_browser)
|
||||
|
||||
_enable_dnssec(session_browser, False)
|
||||
assert not _get_dnssec(session_browser)
|
||||
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup(session_browser):
|
||||
"""Test backup and restore."""
|
||||
functional.app_enable(session_browser, 'bind')
|
||||
functional.set_forwarders(session_browser, '1.1.1.1')
|
||||
_enable_dnssec(session_browser, False)
|
||||
functional.backup_create(session_browser, 'bind', 'test_bind')
|
||||
|
||||
functional.set_forwarders(session_browser, '1.0.0.1')
|
||||
_enable_dnssec(session_browser, True)
|
||||
|
||||
functional.backup_restore(session_browser, 'bind', 'test_bind')
|
||||
assert functional.get_forwarders(session_browser) == '1.1.1.1'
|
||||
assert not _get_dnssec(session_browser)
|
||||
functional.backup_restore(session_browser, 'bind', 'test_bind')
|
||||
assert functional.get_forwarders(session_browser) == '1.1.1.1'
|
||||
assert not _get_dnssec(session_browser)
|
||||
|
||||
|
||||
def _enable_dnssec(browser, enable):
|
||||
|
||||
@ -16,6 +16,7 @@ from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import manifest
|
||||
@ -78,6 +79,9 @@ class CalibreApp(app_module.App):
|
||||
allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-calibre', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-calibre', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
@ -13,63 +13,45 @@ from plinth.tests import functional
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.sso, pytest.mark.calibre]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
"""Login and install the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'calibre')
|
||||
yield
|
||||
functional.app_disable(session_browser, 'calibre')
|
||||
class TestCalibreApp(functional.BaseAppTests):
|
||||
app_name = 'calibre'
|
||||
has_service = True
|
||||
has_web = True
|
||||
|
||||
def test_add_delete_library(self, session_browser):
|
||||
"""Test adding/deleting a new library."""
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
_delete_library(session_browser, 'FunctionalTest', True)
|
||||
|
||||
def test_enable_disable(session_browser):
|
||||
"""Test enabling the app."""
|
||||
functional.app_disable(session_browser, 'calibre')
|
||||
_add_library(session_browser, 'FunctionalTest')
|
||||
assert _is_library_available(session_browser, 'FunctionalTest')
|
||||
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
assert functional.service_is_running(session_browser, 'calibre')
|
||||
assert functional.is_available(session_browser, 'calibre')
|
||||
_delete_library(session_browser, 'FunctionalTest')
|
||||
assert not _is_library_available(session_browser, 'FunctionalTest')
|
||||
|
||||
functional.app_disable(session_browser, 'calibre')
|
||||
assert not functional.service_is_running(session_browser, 'calibre')
|
||||
assert not functional.is_available(session_browser, 'calibre')
|
||||
def test_add_delete_book(self, session_browser):
|
||||
"""Test adding/delete book in the library."""
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
_add_library(session_browser, 'FunctionalTest')
|
||||
_delete_book(session_browser, 'FunctionalTest', 'sample.txt', True)
|
||||
|
||||
|
||||
def test_add_delete_library(session_browser):
|
||||
"""Test adding/deleting a new library."""
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
_delete_library(session_browser, 'FunctionalTest', True)
|
||||
|
||||
_add_library(session_browser, 'FunctionalTest')
|
||||
assert _is_library_available(session_browser, 'FunctionalTest')
|
||||
|
||||
_delete_library(session_browser, 'FunctionalTest')
|
||||
assert not _is_library_available(session_browser, 'FunctionalTest')
|
||||
|
||||
|
||||
def test_add_delete_book(session_browser):
|
||||
"""Test adding/delete book in the library."""
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
_add_library(session_browser, 'FunctionalTest')
|
||||
_delete_book(session_browser, 'FunctionalTest', 'sample.txt', True)
|
||||
|
||||
_add_book(session_browser, 'FunctionalTest', 'sample.txt')
|
||||
assert _is_book_available(session_browser, 'FunctionalTest', 'sample.txt')
|
||||
|
||||
_delete_book(session_browser, 'FunctionalTest', 'sample.txt')
|
||||
assert not _is_book_available(session_browser, 'FunctionalTest',
|
||||
_add_book(session_browser, 'FunctionalTest', 'sample.txt')
|
||||
assert _is_book_available(session_browser, 'FunctionalTest',
|
||||
'sample.txt')
|
||||
|
||||
_delete_book(session_browser, 'FunctionalTest', 'sample.txt')
|
||||
assert not _is_book_available(session_browser, 'FunctionalTest',
|
||||
'sample.txt')
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup(session_browser):
|
||||
"""Test backing up and restoring."""
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
_add_library(session_browser, 'FunctionalTest')
|
||||
functional.backup_create(session_browser, 'calibre', 'test_calibre')
|
||||
_delete_library(session_browser, 'FunctionalTest')
|
||||
functional.backup_restore(session_browser, 'calibre', 'test_calibre')
|
||||
assert _is_library_available(session_browser, 'FunctionalTest')
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(self, session_browser):
|
||||
"""Test backing up and restoring."""
|
||||
functional.app_enable(session_browser, 'calibre')
|
||||
_add_library(session_browser, 'FunctionalTest')
|
||||
functional.backup_create(session_browser, 'calibre', 'test_calibre')
|
||||
_delete_library(session_browser, 'FunctionalTest')
|
||||
functional.backup_restore(session_browser, 'calibre', 'test_calibre')
|
||||
assert _is_library_available(session_browser, 'FunctionalTest')
|
||||
|
||||
|
||||
def _add_library(browser, name):
|
||||
|
||||
@ -14,6 +14,7 @@ from plinth.modules import names
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
from plinth.signals import domain_added, domain_removed
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
@ -60,6 +61,7 @@ class CockpitApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
is_essential=is_essential, name=_('Cockpit'),
|
||||
icon='fa-wrench', icon_filename='cockpit',
|
||||
@ -81,6 +83,9 @@ class CockpitApp(app_module.App):
|
||||
allowed_groups=['admin'])
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-cockpit', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-cockpit', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
@ -96,6 +101,9 @@ class CockpitApp(app_module.App):
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
@staticmethod
|
||||
def post_init():
|
||||
"""Perform post initialization operations."""
|
||||
domain_added.connect(on_domain_added)
|
||||
domain_removed.connect(on_domain_removed)
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ from plinth import frontpage, menu
|
||||
from plinth.modules.apache import (get_users_with_website, user_of_uws_url,
|
||||
uws_url_of_user)
|
||||
from plinth.modules.names.components import DomainType
|
||||
from plinth.package import Packages
|
||||
from plinth.signals import domain_added
|
||||
|
||||
version = 3
|
||||
@ -64,10 +65,16 @@ class ConfigApp(app_module.App):
|
||||
'config:index', parent_url_name='system')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-config', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
domain_type = DomainType('domain-type-static', _('Domain Name'),
|
||||
'config:index', can_have_certificate=True)
|
||||
self.add(domain_type)
|
||||
|
||||
@staticmethod
|
||||
def post_init():
|
||||
"""Perform post initialization operations."""
|
||||
# Register domain with Name Services module.
|
||||
domainname = get_domainname()
|
||||
if domainname:
|
||||
|
||||
@ -20,6 +20,7 @@ from plinth.modules.coturn.components import TurnConfiguration, TurnConsumer
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.letsencrypt.components import LetsEncrypt
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import manifest
|
||||
@ -69,6 +70,9 @@ class CoturnApp(app_module.App):
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-coturn', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-coturn', info.name,
|
||||
ports=['coturn-freedombox'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
@ -20,8 +20,6 @@ is_essential = True
|
||||
|
||||
managed_services = ['systemd-timesyncd']
|
||||
|
||||
managed_packages = []
|
||||
|
||||
_description = [
|
||||
_('Network time server is a program that maintains the system time '
|
||||
'in synchronization with servers on the Internet.')
|
||||
@ -56,7 +54,7 @@ class DateTimeApp(app_module.App):
|
||||
'--value', 'systemd-timesyncd'
|
||||
])
|
||||
self._time_managed = 'yes' in output.decode()
|
||||
except subprocess.CalledProcessError:
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
# When systemd is not running.
|
||||
self._time_managed = False
|
||||
|
||||
@ -65,6 +63,7 @@ class DateTimeApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
is_essential=is_essential,
|
||||
name=_('Date & Time'), icon='fa-clock-o',
|
||||
|
||||
@ -14,6 +14,7 @@ from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.users import add_user_to_share_group
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import manifest
|
||||
|
||||
@ -69,6 +70,9 @@ class DelugeApp(app_module.App):
|
||||
allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-deluge', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-deluge', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
@ -12,69 +12,50 @@ from plinth.tests import functional
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.deluge]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
"""Login and install the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'deluge')
|
||||
yield
|
||||
functional.app_disable(session_browser, 'deluge')
|
||||
class TestDelugeApp(functional.BaseAppTests):
|
||||
app_name = 'deluge'
|
||||
has_service = True
|
||||
has_web = True
|
||||
|
||||
def test_bittorrent_group(self, session_browser):
|
||||
"""Test if only users in bit-torrent group can access Deluge."""
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
if not functional.user_exists(session_browser, 'delugeuser'):
|
||||
functional.create_user(session_browser, 'delugeuser',
|
||||
groups=['bit-torrent'])
|
||||
|
||||
def test_enable_disable(session_browser):
|
||||
"""Test enabling the app."""
|
||||
functional.app_disable(session_browser, 'deluge')
|
||||
if not functional.user_exists(session_browser, 'nogroupuser'):
|
||||
functional.create_user(session_browser, 'nogroupuser')
|
||||
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
assert functional.service_is_running(session_browser, 'deluge')
|
||||
assert functional.is_available(session_browser, 'deluge')
|
||||
functional.login_with_account(session_browser, functional.base_url,
|
||||
'delugeuser')
|
||||
assert functional.is_available(session_browser, 'deluge')
|
||||
|
||||
functional.app_disable(session_browser, 'deluge')
|
||||
assert functional.service_is_not_running(session_browser, 'deluge')
|
||||
assert not functional.is_available(session_browser, 'deluge')
|
||||
functional.login_with_account(session_browser, functional.base_url,
|
||||
'nogroupuser')
|
||||
assert not functional.is_available(session_browser, 'deluge')
|
||||
|
||||
functional.login(session_browser)
|
||||
|
||||
def test_bittorrent_group(session_browser):
|
||||
"""Test if only users in bit-torrent group can access Deluge."""
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
if not functional.user_exists(session_browser, 'delugeuser'):
|
||||
functional.create_user(session_browser, 'delugeuser',
|
||||
groups=['bit-torrent'])
|
||||
def test_upload_torrent(self, session_browser):
|
||||
"""Test uploading a torrent."""
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
_remove_all_torrents(session_browser)
|
||||
_upload_sample_torrent(session_browser)
|
||||
assert _get_number_of_torrents(session_browser) == 1
|
||||
|
||||
if not functional.user_exists(session_browser, 'nogroupuser'):
|
||||
functional.create_user(session_browser, 'nogroupuser')
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(self, session_browser):
|
||||
"""Test backup and restore."""
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
_remove_all_torrents(session_browser)
|
||||
_upload_sample_torrent(session_browser)
|
||||
functional.backup_create(session_browser, 'deluge', 'test_deluge')
|
||||
|
||||
functional.login_with_account(session_browser, functional.base_url,
|
||||
'delugeuser')
|
||||
assert functional.is_available(session_browser, 'deluge')
|
||||
|
||||
functional.login_with_account(session_browser, functional.base_url,
|
||||
'nogroupuser')
|
||||
assert not functional.is_available(session_browser, 'deluge')
|
||||
|
||||
functional.login(session_browser)
|
||||
|
||||
|
||||
def test_upload_torrent(session_browser):
|
||||
"""Test uploading a torrent."""
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
_remove_all_torrents(session_browser)
|
||||
_upload_sample_torrent(session_browser)
|
||||
assert _get_number_of_torrents(session_browser) == 1
|
||||
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(session_browser):
|
||||
"""Test backup and restore."""
|
||||
functional.app_enable(session_browser, 'deluge')
|
||||
_remove_all_torrents(session_browser)
|
||||
_upload_sample_torrent(session_browser)
|
||||
functional.backup_create(session_browser, 'deluge', 'test_deluge')
|
||||
|
||||
_remove_all_torrents(session_browser)
|
||||
functional.backup_restore(session_browser, 'deluge', 'test_deluge')
|
||||
assert functional.service_is_running(session_browser, 'deluge')
|
||||
assert _get_number_of_torrents(session_browser) == 1
|
||||
_remove_all_torrents(session_browser)
|
||||
functional.backup_restore(session_browser, 'deluge', 'test_deluge')
|
||||
assert functional.service_is_running(session_browser, 'deluge')
|
||||
assert _get_number_of_torrents(session_browser) == 1
|
||||
|
||||
|
||||
def _get_active_window_title(browser):
|
||||
|
||||
@ -64,6 +64,9 @@ class DiagnosticsApp(app_module.App):
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
@staticmethod
|
||||
def post_init():
|
||||
"""Perform post initialization operations."""
|
||||
# Check periodically for low RAM space
|
||||
interval = 180 if cfg.develop else 3600
|
||||
glib.schedule(interval, _warn_about_low_ram_space)
|
||||
|
||||
@ -12,6 +12,7 @@ from plinth.daemon import Daemon
|
||||
from plinth.errors import DomainNotRegisteredError
|
||||
from plinth.modules.apache.components import Webserver, diagnose_url
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
domain_name_file = "/etc/diaspora/domain_name"
|
||||
@ -64,6 +65,7 @@ class DiasporaApp(app_module.App):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
from . import manifest
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
name=_('diaspora*'), icon_filename='diaspora',
|
||||
short_description=_('Federated Social Network'),
|
||||
@ -82,6 +84,9 @@ class DiasporaApp(app_module.App):
|
||||
clients=info.clients, login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-diaspora', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-diaspora', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
@ -11,6 +11,7 @@ from plinth import cfg, menu
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.names.components import DomainType
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.package import Packages
|
||||
from plinth.signals import domain_added
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
@ -51,6 +52,7 @@ class DynamicDNSApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
is_essential=is_essential, depends=depends,
|
||||
name=_('Dynamic DNS Client'), icon='fa-refresh',
|
||||
@ -62,6 +64,9 @@ class DynamicDNSApp(app_module.App):
|
||||
'dynamicdns:index', parent_url_name='system')
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-dynamicdns', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
domain_type = DomainType('domain-type-dynamic',
|
||||
_('Dynamic Domain Name'), 'dynamicdns:index',
|
||||
can_have_certificate=True)
|
||||
@ -75,6 +80,9 @@ class DynamicDNSApp(app_module.App):
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
@staticmethod
|
||||
def post_init():
|
||||
"""Perform post initialization operations."""
|
||||
current_status = get_status()
|
||||
if current_status['enabled']:
|
||||
domain_added.send_robust(sender='dynamicdns',
|
||||
|
||||
@ -21,6 +21,7 @@ from plinth.modules.coturn.components import TurnConfiguration, TurnConsumer
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.letsencrypt.components import LetsEncrypt
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.package import Packages
|
||||
from plinth.signals import (domain_added, post_hostname_change,
|
||||
pre_hostname_change)
|
||||
from plinth.utils import format_lazy
|
||||
@ -67,6 +68,7 @@ class EjabberdApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
name=_('ejabberd'), icon_filename='ejabberd',
|
||||
short_description=_('Chat Server'),
|
||||
@ -88,6 +90,9 @@ class EjabberdApp(app_module.App):
|
||||
login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-ejabberd', managed_packages)
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-ejabberd', info.name,
|
||||
ports=['xmpp-client', 'xmpp-server',
|
||||
'xmpp-bosh'], is_external=True)
|
||||
@ -123,6 +128,9 @@ class EjabberdApp(app_module.App):
|
||||
turn = EjabberdTurnConsumer('turn-ejabberd')
|
||||
self.add(turn)
|
||||
|
||||
@staticmethod
|
||||
def post_init():
|
||||
"""Perform post initialization operations."""
|
||||
pre_hostname_change.connect(on_pre_hostname_change)
|
||||
post_hostname_change.connect(on_post_hostname_change)
|
||||
domain_added.connect(on_domain_added)
|
||||
|
||||
@ -12,47 +12,31 @@ pytestmark = [pytest.mark.apps, pytest.mark.ejabberd]
|
||||
# TODO Check domain name display
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
"""Login and install the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'ejabberd')
|
||||
yield
|
||||
functional.app_disable(session_browser, 'ejabberd')
|
||||
class TestEjabberdApp(functional.BaseAppTests):
|
||||
app_name = 'ejabberd'
|
||||
has_service = True
|
||||
has_web = False
|
||||
|
||||
def test_message_archive_management(self, session_browser):
|
||||
"""Test enabling message archive management."""
|
||||
functional.app_enable(session_browser, 'ejabberd')
|
||||
_enable_message_archive_management(session_browser)
|
||||
assert functional.service_is_running(session_browser, 'ejabberd')
|
||||
|
||||
def test_enable_disable(session_browser):
|
||||
"""Test enabling the app."""
|
||||
functional.app_disable(session_browser, 'ejabberd')
|
||||
_disable_message_archive_management(session_browser)
|
||||
assert functional.service_is_running(session_browser, 'ejabberd')
|
||||
|
||||
functional.app_enable(session_browser, 'ejabberd')
|
||||
assert functional.service_is_running(session_browser, 'ejabberd')
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(self, session_browser):
|
||||
"""Test backup and restore of app data."""
|
||||
functional.app_enable(session_browser, 'ejabberd')
|
||||
_jsxc_add_contact(session_browser)
|
||||
functional.backup_create(session_browser, 'ejabberd', 'test_ejabberd')
|
||||
|
||||
functional.app_disable(session_browser, 'ejabberd')
|
||||
assert functional.service_is_not_running(session_browser, 'ejabberd')
|
||||
_jsxc_delete_contact(session_browser)
|
||||
functional.backup_restore(session_browser, 'ejabberd', 'test_ejabberd')
|
||||
|
||||
|
||||
def test_message_archive_management(session_browser):
|
||||
"""Test enabling message archive management."""
|
||||
functional.app_enable(session_browser, 'ejabberd')
|
||||
_enable_message_archive_management(session_browser)
|
||||
assert functional.service_is_running(session_browser, 'ejabberd')
|
||||
|
||||
_disable_message_archive_management(session_browser)
|
||||
assert functional.service_is_running(session_browser, 'ejabberd')
|
||||
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup_restore(session_browser):
|
||||
"""Test backup and restore of app data."""
|
||||
functional.app_enable(session_browser, 'ejabberd')
|
||||
_jsxc_add_contact(session_browser)
|
||||
functional.backup_create(session_browser, 'ejabberd', 'test_ejabberd')
|
||||
|
||||
_jsxc_delete_contact(session_browser)
|
||||
functional.backup_restore(session_browser, 'ejabberd', 'test_ejabberd')
|
||||
|
||||
_jsxc_assert_has_contact(session_browser)
|
||||
_jsxc_assert_has_contact(session_browser)
|
||||
|
||||
|
||||
def _enable_message_archive_management(browser):
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import plinth.app
|
||||
@ -30,6 +29,7 @@ package_conflicts_action = 'ignore'
|
||||
|
||||
packages = [
|
||||
'postfix-ldap',
|
||||
'postfix-sqlite',
|
||||
'dovecot-pop3d',
|
||||
'dovecot-imapd',
|
||||
'dovecot-ldap',
|
||||
@ -82,17 +82,13 @@ class EmailServerApp(plinth.app.App):
|
||||
self.add(webserver)
|
||||
|
||||
# Let's Encrypt event hook
|
||||
default_domain = get_domainname()
|
||||
domains = [default_domain] if default_domain else []
|
||||
letsencrypt = LetsEncrypt('letsencrypt-email-server', domains=domains,
|
||||
letsencrypt = LetsEncrypt('letsencrypt-email-server',
|
||||
domains=get_domains,
|
||||
daemons=['postfix', 'dovecot'],
|
||||
should_copy_certificates=False,
|
||||
managing_app='email_server')
|
||||
self.add(letsencrypt)
|
||||
|
||||
if not domains:
|
||||
logger.warning('Could not fetch the FreedomBox domain name!')
|
||||
|
||||
def _add_ui_components(self):
|
||||
info = plinth.app.Info(
|
||||
app_id=self.app_id, version=version, name=self.app_name,
|
||||
@ -111,13 +107,6 @@ class EmailServerApp(plinth.app.App):
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = plinth.frontpage.Shortcut(
|
||||
'shortcut_' + self.app_id, name=info.name,
|
||||
short_description=info.short_description, icon='roundcube',
|
||||
url=reverse_lazy('email_server:my_mail'), clients=manifest.clients,
|
||||
login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
def _add_daemons(self):
|
||||
for srvname in managed_services:
|
||||
# Construct `listen_ports` parameter for the daemon
|
||||
@ -153,6 +142,12 @@ class EmailServerApp(plinth.app.App):
|
||||
return results
|
||||
|
||||
|
||||
def get_domains():
|
||||
"""Return the list of domains configured."""
|
||||
default_domain = get_domainname()
|
||||
return [default_domain] if default_domain else []
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Installs and configures module"""
|
||||
|
||||
@ -169,6 +164,7 @@ def setup(helper, old_version=None):
|
||||
helper.install(packages_bloat, skip_recommends=True)
|
||||
|
||||
# Setup
|
||||
helper.call('post', audit.home.repair)
|
||||
helper.call('post', audit.domain.repair)
|
||||
helper.call('post', audit.ldap.repair)
|
||||
helper.call('post', audit.spam.repair)
|
||||
|
||||
115
plinth/modules/email_server/aliases.py
Normal file
115
plinth/modules/email_server/aliases.py
Normal file
@ -0,0 +1,115 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Manage email aliases."""
|
||||
|
||||
import contextlib
|
||||
import pwd
|
||||
import sqlite3
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
|
||||
from plinth import actions
|
||||
|
||||
|
||||
@dataclass
|
||||
class Alias:
|
||||
value: str
|
||||
name: str
|
||||
enabled: bool = field(init=False)
|
||||
status: InitVar[int]
|
||||
|
||||
def __post_init__(self, status):
|
||||
self.enabled = (status != 0)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _get_cursor():
|
||||
"""Return a DB cursor as context manager."""
|
||||
# Turn ON autocommit mode
|
||||
db_path = '/var/lib/postfix/freedombox-aliases/aliases.sqlite3'
|
||||
connection = sqlite3.connect(db_path, isolation_level=None)
|
||||
connection.row_factory = sqlite3.Row
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
yield cursor
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def get(username):
|
||||
"""Get all aliases of a user."""
|
||||
query = 'SELECT name, value, status FROM alias WHERE value=?'
|
||||
with _get_cursor() as cursor:
|
||||
rows = cursor.execute(query, (username, ))
|
||||
return [Alias(**row) for row in rows]
|
||||
|
||||
|
||||
def exists(name):
|
||||
"""Return whether alias is already taken."""
|
||||
try:
|
||||
pwd.getpwnam(name)
|
||||
return True
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
with _get_cursor() as cursor:
|
||||
query = 'SELECT COUNT(*) FROM alias WHERE name=?'
|
||||
cursor.execute(query, (name, ))
|
||||
return cursor.fetchone()[0] != 0
|
||||
|
||||
|
||||
def put(username, name):
|
||||
"""Insert if not exists a new alias."""
|
||||
query = 'INSERT INTO alias (name, value, status) VALUES (?, ?, ?)'
|
||||
with _get_cursor() as cursor:
|
||||
try:
|
||||
cursor.execute(query, (name, username, 1))
|
||||
except sqlite3.IntegrityError:
|
||||
pass # Alias exists, rare since we are already checking
|
||||
|
||||
|
||||
def delete(username, aliases):
|
||||
"""Delete a set of aliases."""
|
||||
query = 'DELETE FROM alias WHERE value=? AND name=?'
|
||||
parameter_seq = ((username, name) for name in aliases)
|
||||
with _get_cursor() as cursor:
|
||||
cursor.execute('BEGIN')
|
||||
cursor.executemany(query, parameter_seq)
|
||||
cursor.execute('COMMIT')
|
||||
|
||||
|
||||
def enable(username, aliases):
|
||||
"""Enable a list of aliases."""
|
||||
return _set_status(username, aliases, 1)
|
||||
|
||||
|
||||
def disable(username, aliases):
|
||||
"""Disable a list of aliases."""
|
||||
return _set_status(username, aliases, 0)
|
||||
|
||||
|
||||
def _set_status(username, aliases, status):
|
||||
"""Set the status value of a list of aliases."""
|
||||
query = 'UPDATE alias SET status=? WHERE value=? AND name=?'
|
||||
parameter_seq = ((status, username, name) for name in aliases)
|
||||
with _get_cursor() as cursor:
|
||||
cursor.execute('BEGIN')
|
||||
cursor.executemany(query, parameter_seq)
|
||||
cursor.execute('COMMIT')
|
||||
|
||||
|
||||
def first_setup():
|
||||
"""Create the database file and schema inside it."""
|
||||
actions.superuser_run('email_server', ['aliases', 'setup'])
|
||||
|
||||
# Create schema if not exists
|
||||
query = '''
|
||||
BEGIN;
|
||||
CREATE TABLE IF NOT EXISTS alias (
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
PRIMARY KEY (name)
|
||||
);
|
||||
COMMIT;
|
||||
'''
|
||||
with _get_cursor() as cursor:
|
||||
cursor.executescript(query)
|
||||
@ -1,152 +0,0 @@
|
||||
"""Manages email aliases"""
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import contextlib
|
||||
import dbm
|
||||
import logging
|
||||
import os
|
||||
import pwd
|
||||
import sqlite3
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.modules.email_server import lock
|
||||
|
||||
from . import models
|
||||
|
||||
map_db_schema_script = """
|
||||
PRAGMA journal_mode=WAL;
|
||||
BEGIN;
|
||||
CREATE TABLE IF NOT EXISTS Alias (
|
||||
email_name TEXT NOT NULL,
|
||||
uid_number INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
PRIMARY KEY (email_name)
|
||||
);
|
||||
COMMIT;
|
||||
"""
|
||||
|
||||
mailsrv_dir = '/var/lib/plinth/mailsrv'
|
||||
hash_db_path = mailsrv_dir + '/aliases'
|
||||
sqlite_db_path = mailsrv_dir + '/aliases.sqlite3'
|
||||
|
||||
alias_sync_mutex = lock.Mutex('alias-sync')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def db_cursor():
|
||||
# Turn ON autocommit mode
|
||||
con = sqlite3.connect(sqlite_db_path, isolation_level=None)
|
||||
con.row_factory = sqlite3.Row
|
||||
try:
|
||||
cur = con.cursor()
|
||||
yield cur
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def get(uid_number):
|
||||
s = 'SELECT * FROM Alias WHERE uid_number=?'
|
||||
with db_cursor() as cur:
|
||||
rows = cur.execute(s, (uid_number, ))
|
||||
result = [models.Alias(**r) for r in rows]
|
||||
return result
|
||||
|
||||
|
||||
def put(uid_number, email_name):
|
||||
s = """INSERT INTO Alias(email_name, uid_number, status)
|
||||
SELECT ?,?,? WHERE NOT EXISTS(
|
||||
SELECT 1 FROM Alias WHERE email_name=?
|
||||
)"""
|
||||
email_name = models.sanitize_email_name(email_name)
|
||||
# email_name cannot be the same as a user name
|
||||
try:
|
||||
pwd.getpwnam(email_name)
|
||||
raise ValidationError(_('The alias was taken'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
with db_cursor() as cur:
|
||||
cur.execute(s, (email_name, uid_number, 1, email_name))
|
||||
if cur.rowcount == 0:
|
||||
raise ValidationError(_('The alias was taken'))
|
||||
|
||||
schedule_hash_update()
|
||||
|
||||
|
||||
def delete(uid_number, alias_list):
|
||||
s = 'DELETE FROM Alias WHERE uid_number=? AND email_name=?'
|
||||
for i in range(len(alias_list)):
|
||||
alias_list[i] = models.sanitize_email_name(alias_list[i])
|
||||
|
||||
parameter_seq = ((uid_number, a) for a in alias_list)
|
||||
with db_cursor() as cur:
|
||||
cur.execute('BEGIN')
|
||||
cur.executemany(s, parameter_seq)
|
||||
cur.execute('COMMIT')
|
||||
schedule_hash_update()
|
||||
|
||||
|
||||
def set_enabled(uid_number, alias_list):
|
||||
return _set_status(uid_number, alias_list, 1)
|
||||
|
||||
|
||||
def set_disabled(uid_number, alias_list):
|
||||
return _set_status(uid_number, alias_list, 0)
|
||||
|
||||
|
||||
def _set_status(uid_number, alias_list, status):
|
||||
s = 'UPDATE Alias SET status=? WHERE uid_number=? AND email_name=?'
|
||||
for i in range(len(alias_list)):
|
||||
alias_list[i] = models.sanitize_email_name(alias_list[i])
|
||||
|
||||
parameter_seq = ((status, uid_number, a) for a in alias_list)
|
||||
with db_cursor() as cur:
|
||||
cur.execute('BEGIN')
|
||||
cur.executemany(s, parameter_seq)
|
||||
cur.execute('COMMIT')
|
||||
schedule_hash_update()
|
||||
|
||||
|
||||
def schedule_hash_update():
|
||||
tmp = hash_db_path + '-tmp'
|
||||
with alias_sync_mutex.lock_all(), db_cursor() as cur:
|
||||
all_aliases = cur.execute('SELECT * FROM Alias')
|
||||
|
||||
# Delete the temp file if exists
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
|
||||
# Create new alias db at temp path
|
||||
db = dbm.ndbm.open(tmp, 'c')
|
||||
try:
|
||||
for row in all_aliases:
|
||||
alias = models.Alias(**row)
|
||||
key = alias.email_name.encode('ascii') + b'\0'
|
||||
if alias.enabled:
|
||||
value = str(alias.uid_number).encode('ascii')
|
||||
value += b'@localhost\0'
|
||||
else:
|
||||
value = b'/dev/null\0'
|
||||
db[key] = value
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Atomically replace old alias db, rename(2)
|
||||
os.rename(tmp + '.db', hash_db_path + '.db')
|
||||
|
||||
|
||||
def first_setup():
|
||||
_create_db_schema_if_not_exists()
|
||||
schedule_hash_update()
|
||||
|
||||
|
||||
def _create_db_schema_if_not_exists():
|
||||
# Create folder
|
||||
if not os.path.isdir(mailsrv_dir):
|
||||
os.mkdir(mailsrv_dir)
|
||||
# Create schema if not exists
|
||||
with db_cursor() as cur:
|
||||
cur.executescript(map_db_schema_script)
|
||||
@ -1,31 +0,0 @@
|
||||
import re
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
email_positive_pattern = re.compile('^[a-zA-Z0-9-_\\.]+')
|
||||
|
||||
|
||||
def sanitize_email_name(email_name):
|
||||
email_name = email_name.strip().lower()
|
||||
if len(email_name) < 2:
|
||||
raise ValidationError(_('Must be at least 2 characters long'))
|
||||
if not re.match('^[a-z0-9-_\\.]+$', email_name):
|
||||
raise ValidationError(_('Contains illegal characters'))
|
||||
if not re.match('^[a-z0-9].*[a-z0-9]$', email_name):
|
||||
raise ValidationError(_('Must start and end with a-z or 0-9'))
|
||||
if re.match('^[0-9]+$', email_name):
|
||||
raise ValidationError(_('Cannot be a number'))
|
||||
return email_name
|
||||
|
||||
|
||||
@dataclass
|
||||
class Alias:
|
||||
uid_number: int
|
||||
email_name: str
|
||||
enabled: bool = field(init=False)
|
||||
status: InitVar[int]
|
||||
|
||||
def __post_init__(self, status):
|
||||
self.enabled = (status != 0)
|
||||
@ -3,6 +3,8 @@
|
||||
Provides diagnosis and repair of email server configuration issues
|
||||
"""
|
||||
|
||||
from . import domain, home, ldap, models, rcube, spam, tls
|
||||
from . import aliases, domain, home, ldap, models, rcube, spam, tls
|
||||
|
||||
__all__ = ['domain', 'home', 'ldap', 'models', 'rcube', 'spam', 'tls']
|
||||
__all__ = [
|
||||
'aliases', 'domain', 'home', 'ldap', 'models', 'rcube', 'spam', 'tls'
|
||||
]
|
||||
|
||||
12
plinth/modules/email_server/audit/aliases.py
Normal file
12
plinth/modules/email_server/audit/aliases.py
Normal file
@ -0,0 +1,12 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Privileged operations for managing aliases."""
|
||||
|
||||
import pathlib
|
||||
import shutil
|
||||
|
||||
|
||||
def action_setup():
|
||||
"""Create a the sqlite3 database to be managed by FreedomBox."""
|
||||
path = pathlib.Path('/var/lib/postfix/freedombox-aliases/')
|
||||
path.mkdir(mode=0o750, exist_ok=True)
|
||||
shutil.chown(path, user='plinth', group='postfix')
|
||||
@ -4,12 +4,12 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -42,14 +42,14 @@ def get():
|
||||
|
||||
|
||||
def repair():
|
||||
superuser_run('email_server', ['-i', 'domain', 'set_up'])
|
||||
superuser_run('email_server', ['domain', 'set_up'])
|
||||
|
||||
|
||||
def repair_component(action_name):
|
||||
allowed_actions = {'set_up': ['postfix']}
|
||||
if action_name not in allowed_actions:
|
||||
return
|
||||
superuser_run('email_server', ['-i', 'domain', action_name])
|
||||
superuser_run('email_server', ['domain', action_name])
|
||||
return allowed_actions[action_name]
|
||||
|
||||
|
||||
@ -181,22 +181,11 @@ def _apply_domain_changes(conf_dict):
|
||||
|
||||
|
||||
def get_domain_config():
|
||||
fields = []
|
||||
|
||||
# Special keys
|
||||
mailname = SimpleNamespace(key='_mailname', name='/etc/mailname')
|
||||
with open('/etc/mailname', 'r') as fd:
|
||||
mailname.value = fd.readline().strip()
|
||||
fields.append(mailname)
|
||||
|
||||
# Postconf keys
|
||||
postconf_keys = [k for k in managed_keys if not k.startswith('_')]
|
||||
result_dict = postconf.get_many(postconf_keys)
|
||||
for key, value in result_dict.items():
|
||||
field = SimpleNamespace(key=key, value=value, name='$' + key)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
"""Return the current domain configuration."""
|
||||
postconf_keys = [key for key in managed_keys if not key.startswith('_')]
|
||||
config = postconf.get_many(postconf_keys)
|
||||
config['_mailname'] = pathlib.Path('/etc/mailname').read_text().strip()
|
||||
return config
|
||||
|
||||
|
||||
def set_keys(raw):
|
||||
@ -210,7 +199,7 @@ def set_keys(raw):
|
||||
raise ClientError('POST data exceeds max line length')
|
||||
|
||||
try:
|
||||
superuser_run('email_server', ['-i', 'domain', 'set_keys'], input=ipc)
|
||||
superuser_run('email_server', ['domain', 'set_keys'], input=ipc)
|
||||
except ActionError as e:
|
||||
stdout = e.args[1]
|
||||
if not stdout.startswith('ClientError:'):
|
||||
|
||||
@ -1,71 +1,22 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Privileged actions to setup users' dovecot mail home directory."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pwd
|
||||
import subprocess
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.actions import superuser_run
|
||||
from plinth.errors import ActionError
|
||||
from plinth.modules.email_server import interproc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from plinth import actions
|
||||
|
||||
|
||||
def exists_nam(username):
|
||||
"""Returns True if the user's home directory exists"""
|
||||
try:
|
||||
passwd = pwd.getpwnam(username)
|
||||
except KeyError as e:
|
||||
raise ValidationError(_('User does not exist')) from e
|
||||
return _exists(passwd)
|
||||
def repair():
|
||||
"""Set correct permissions on /var/mail/ directory.
|
||||
|
||||
For each user, /var/mail/<user> is the 'dovecot mail home' for that user.
|
||||
Dovecot creates new directories with the same permissions as the parent
|
||||
directory. Ensure that 'others' can access /var/mail/.
|
||||
|
||||
"""
|
||||
actions.superuser_run('email_server', ['home', 'set_up'])
|
||||
|
||||
|
||||
def exists_uid(uid_number):
|
||||
"""Returns True if the user's home directory exists"""
|
||||
try:
|
||||
passwd = pwd.getpwuid(uid_number)
|
||||
except KeyError as e:
|
||||
raise ValidationError(_('User does not exist')) from e
|
||||
return _exists(passwd)
|
||||
|
||||
|
||||
def _exists(passwd):
|
||||
return os.path.exists(passwd.pw_dir)
|
||||
|
||||
|
||||
def put_nam(username):
|
||||
"""Create a home directory for the user (identified by username)"""
|
||||
_put('nam', username)
|
||||
|
||||
|
||||
def put_uid(uid_number):
|
||||
"""Create a home directory for the user (identified by UID)"""
|
||||
_put('uid', str(uid_number))
|
||||
|
||||
|
||||
def _put(arg_type, user_info):
|
||||
try:
|
||||
args = ['-i', 'home', 'mk', arg_type, user_info]
|
||||
superuser_run('email_server', args)
|
||||
except ActionError as e:
|
||||
raise RuntimeError('Action script failure') from e
|
||||
|
||||
|
||||
def action_mk(arg_type, user_info):
|
||||
if arg_type == 'nam':
|
||||
passwd = pwd.getpwnam(user_info)
|
||||
elif arg_type == 'uid':
|
||||
passwd = pwd.getpwuid(int(user_info))
|
||||
else:
|
||||
raise ValueError('Unknown arg_type')
|
||||
|
||||
args = ['sudo', '-n', '--user=#' + str(passwd.pw_uid)]
|
||||
args.extend(['/bin/sh', '-c', 'mkdir -p ~'])
|
||||
completed = subprocess.run(args, capture_output=True, check=False)
|
||||
if completed.returncode != 0:
|
||||
interproc.log_subprocess(completed)
|
||||
raise OSError('Could not create home directory')
|
||||
def action_set_up():
|
||||
"""Run chmod on /var/mail to remove all permissions for 'others'."""
|
||||
subprocess.run(['chmod', 'o-rwx', '/var/mail'], check=True)
|
||||
|
||||
@ -13,21 +13,27 @@ from plinth import actions
|
||||
from . import models
|
||||
|
||||
default_config = {
|
||||
'smtpd_sasl_auth_enable': 'yes',
|
||||
'smtpd_sasl_type': 'dovecot',
|
||||
'smtpd_sasl_path': 'private/auth',
|
||||
'mailbox_transport': 'lmtp:unix:private/dovecot-lmtp',
|
||||
'virtual_transport': 'lmtp:unix:private/dovecot-lmtp',
|
||||
|
||||
'smtpd_relay_restrictions': ','.join([
|
||||
'permit_sasl_authenticated', 'defer_unauth_destination',
|
||||
])
|
||||
'smtpd_sasl_auth_enable':
|
||||
'yes',
|
||||
'smtpd_sasl_type':
|
||||
'dovecot',
|
||||
'smtpd_sasl_path':
|
||||
'private/auth',
|
||||
'mailbox_transport':
|
||||
'lmtp:unix:private/dovecot-lmtp',
|
||||
'virtual_transport':
|
||||
'lmtp:unix:private/dovecot-lmtp',
|
||||
'smtpd_relay_restrictions':
|
||||
','.join([
|
||||
'permit_sasl_authenticated',
|
||||
'defer_unauth_destination',
|
||||
])
|
||||
}
|
||||
|
||||
submission_flags = postconf.ServiceFlags(
|
||||
service='submission', type='inet', private='n', unpriv='-', chroot='y',
|
||||
wakeup='-', maxproc='-', command_args='smtpd'
|
||||
)
|
||||
submission_flags = postconf.ServiceFlags(service='submission', type='inet',
|
||||
private='n', unpriv='-', chroot='y',
|
||||
wakeup='-', maxproc='-',
|
||||
command_args='smtpd')
|
||||
|
||||
default_submission_options = {
|
||||
'syslog_name': 'postfix/submission',
|
||||
@ -36,10 +42,9 @@ default_submission_options = {
|
||||
'smtpd_relay_restrictions': 'permit_sasl_authenticated,reject'
|
||||
}
|
||||
|
||||
smtps_flags = postconf.ServiceFlags(
|
||||
service='smtps', type='inet', private='n', unpriv='-', chroot='y',
|
||||
wakeup='-', maxproc='-', command_args='smtpd'
|
||||
)
|
||||
smtps_flags = postconf.ServiceFlags(service='smtps', type='inet', private='n',
|
||||
unpriv='-', chroot='y', wakeup='-',
|
||||
maxproc='-', command_args='smtpd')
|
||||
|
||||
default_smtps_options = {
|
||||
'syslog_name': 'postfix/smtps',
|
||||
@ -49,9 +54,7 @@ default_smtps_options = {
|
||||
}
|
||||
|
||||
MAILSRV_DIR = '/var/lib/plinth/mailsrv'
|
||||
ETC_ALIASES = 'hash:/etc/aliases'
|
||||
BEFORE_ALIASES = 'ldap:/etc/postfix/freedombox-username-to-uid-number.cf'
|
||||
AFTER_ALIASES = 'hash:' + aliases.hash_db_path
|
||||
SQLITE_ALIASES = 'sqlite:/etc/postfix/freedombox-aliases.cf'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -65,7 +68,6 @@ def get():
|
||||
translation_table = [
|
||||
(check_sasl, _('Postfix-Dovecot SASL integration')),
|
||||
(check_alias_maps, _('Postfix alias maps')),
|
||||
(check_local_recipient_maps, _('Postfix local recipient maps')),
|
||||
]
|
||||
results = []
|
||||
with postconf.mutex.lock_all():
|
||||
@ -81,7 +83,7 @@ def repair():
|
||||
POST /audit/ldap/repair
|
||||
"""
|
||||
aliases.first_setup()
|
||||
actions.superuser_run('email_server', ['-i', 'ldap', 'set_up'])
|
||||
actions.superuser_run('email_server', ['ldap', 'set_up'])
|
||||
|
||||
|
||||
def action_set_up():
|
||||
@ -118,67 +120,23 @@ def check_alias_maps(title=''):
|
||||
"""Check the ability to mail to usernames and user aliases"""
|
||||
diagnosis = models.MainCfDiagnosis(title)
|
||||
|
||||
analysis = models.AliasMapsAnalysis()
|
||||
analysis.parsed = postconf.parse_maps_by_key_unsafe('alias_maps')
|
||||
analysis.isystem = list_find(analysis.parsed, ETC_ALIASES)
|
||||
analysis.ibefore = list_find(analysis.parsed, BEFORE_ALIASES)
|
||||
analysis.iafter = list_find(analysis.parsed, AFTER_ALIASES)
|
||||
|
||||
if analysis.ibefore == -1 or analysis.iafter == -1:
|
||||
diagnosis.flag_once('alias_maps', user=analysis)
|
||||
alias_maps = postconf.get_unsafe('alias_maps').replace(',', ' ').split(' ')
|
||||
if SQLITE_ALIASES not in alias_maps:
|
||||
diagnosis.flag_once('alias_maps', user=alias_maps)
|
||||
diagnosis.critical('Required maps not in list')
|
||||
if analysis.ibefore > analysis.iafter:
|
||||
diagnosis.flag_once('alias_maps', user=analysis)
|
||||
diagnosis.critical('Insecure map order')
|
||||
|
||||
return diagnosis
|
||||
|
||||
|
||||
def fix_alias_maps(diagnosis):
|
||||
diagnosis.repair('alias_maps', rearrange_alias_maps)
|
||||
diagnosis.apply_changes(postconf.set_many_unsafe)
|
||||
|
||||
def fix_value(alias_maps):
|
||||
if SQLITE_ALIASES not in alias_maps:
|
||||
alias_maps.append(SQLITE_ALIASES)
|
||||
|
||||
def rearrange_alias_maps(analysis):
|
||||
# Delete *all* references to BEFORE_ALIASES and AFTER_ALIASES
|
||||
for i in range(len(analysis.parsed)):
|
||||
if analysis.parsed[i] in (BEFORE_ALIASES, AFTER_ALIASES):
|
||||
analysis.parsed[i] = ''
|
||||
# Does hash:/etc/aliases exist in list?
|
||||
if analysis.isystem >= 0:
|
||||
# Put the maps around hash:/etc/aliases
|
||||
val = '%s %s %s' % (BEFORE_ALIASES, ETC_ALIASES, AFTER_ALIASES)
|
||||
analysis.parsed[analysis.isystem] = val
|
||||
else:
|
||||
# To the end
|
||||
analysis.parsed.append(BEFORE_ALIASES)
|
||||
analysis.parsed.append(AFTER_ALIASES)
|
||||
# List -> string
|
||||
return ' '.join(filter(None, analysis.parsed))
|
||||
return ' '.join(alias_maps)
|
||||
|
||||
|
||||
def check_local_recipient_maps(title=''):
|
||||
diagnosis = models.MainCfDiagnosis(title)
|
||||
lrcpt_maps = postconf.parse_maps_by_key_unsafe('local_recipient_maps')
|
||||
list_modified = False
|
||||
|
||||
# Block mails to system users
|
||||
# local_recipient_maps must not contain proxy:unix:passwd.byname
|
||||
ipasswd = list_find(lrcpt_maps, 'proxy:unix:passwd.byname')
|
||||
if ipasswd >= 0:
|
||||
diagnosis.critical('Mail to system users (/etc/passwd) possible')
|
||||
# Propose a fix
|
||||
lrcpt_maps[ipasswd] = ''
|
||||
list_modified = True
|
||||
|
||||
if list_modified:
|
||||
fix = ' '.join(filter(None, lrcpt_maps))
|
||||
diagnosis.flag('local_recipient_maps', corrected_value=fix)
|
||||
|
||||
return diagnosis
|
||||
|
||||
|
||||
def fix_local_recipient_maps(diagnosis):
|
||||
diagnosis.repair('alias_maps', fix_value)
|
||||
diagnosis.apply_changes(postconf.set_many_unsafe)
|
||||
|
||||
|
||||
@ -186,7 +144,6 @@ def action_set_ulookup():
|
||||
"""Handles email_server -i ldap set_ulookup"""
|
||||
with postconf.mutex.lock_all():
|
||||
fix_alias_maps(check_alias_maps())
|
||||
fix_local_recipient_maps(check_local_recipient_maps())
|
||||
|
||||
|
||||
def list_find(lst, element, start=None, end=None):
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Models of the audit module"""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import typing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -207,14 +205,3 @@ class MainCfDiagnosis(Diagnosis):
|
||||
contains an unresolved issue (i.e. an uncorrected key)"""
|
||||
if None in self.advice.values():
|
||||
raise UnresolvedIssueError('Assertion failed')
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=False)
|
||||
class AliasMapsAnalysis:
|
||||
parsed = typing.List[str]
|
||||
ibefore = int
|
||||
isystem = int
|
||||
iafter = int
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@ -32,7 +32,7 @@ def get():
|
||||
'rc_config_header': _('RoundCube configured for FreedomBox email'),
|
||||
}
|
||||
|
||||
output = actions.superuser_run('email_server', ['-i', 'rcube', 'check'])
|
||||
output = actions.superuser_run('email_server', ['rcube', 'check'])
|
||||
results = json.loads(output)
|
||||
for i in range(0, len(results)):
|
||||
results[i] = models.Diagnosis.from_json(results[i], translation.get)
|
||||
@ -41,14 +41,14 @@ def get():
|
||||
|
||||
|
||||
def repair():
|
||||
actions.superuser_run('email_server', ['-i', 'rcube', 'set_up'])
|
||||
actions.superuser_run('email_server', ['rcube', 'set_up'])
|
||||
|
||||
|
||||
def repair_component(action):
|
||||
action_to_services = {'set_up': []}
|
||||
if action not in action_to_services:
|
||||
return
|
||||
actions.superuser_run('email_server', ['-i', 'rcube', action])
|
||||
actions.superuser_run('email_server', ['rcube', action])
|
||||
return action_to_services[action]
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user