From 5389303e982dd6249897b6240a5df5e24426e33a Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 2 Sep 2022 11:19:21 -0700 Subject: [PATCH] email: Use privileged decorator for actions Tests: - Functional tests work (uninstall test does not work) - Initial setup works - Domains are setup - Home is setup (others don't have permission for /var/mail) - Aliases configuration is setup - Postfix is setup - rspamd is setup - Changing primary domain works - Adding/removing domains works - Error during operations is handle properly: getting dkim key - Setting up DKIM key when changing, adding/removing domain works - Showing DKIM key in app page works Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- actions/email | 66 --------------------- plinth/modules/email/__init__.py | 20 +++---- plinth/modules/email/aliases.py | 8 +-- plinth/modules/email/dns.py | 7 +-- plinth/modules/email/privileged/__init__.py | 18 +++--- plinth/modules/email/privileged/aliases.py | 9 +-- plinth/modules/email/privileged/dkim.py | 20 +++---- plinth/modules/email/privileged/domain.py | 17 +++--- plinth/modules/email/privileged/home.py | 13 ++-- plinth/modules/email/privileged/postfix.py | 20 +++---- plinth/modules/email/privileged/spam.py | 10 +--- plinth/modules/email/views.py | 2 +- 12 files changed, 63 insertions(+), 147 deletions(-) delete mode 100755 actions/email diff --git a/actions/email b/actions/email deleted file mode 100755 index 28704472b..000000000 --- a/actions/email +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Configuration helper for email server. -""" - -import argparse -import logging -import os -import sys - -import plinth.log -from plinth.modules.email import privileged - -EXIT_SYNTAX = 10 -EXIT_PERM = 20 - -logger = logging.getLogger(__file__) - - -def main(): - """Parse arguments.""" - plinth.log.action_init() - - parser = argparse.ArgumentParser() - 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() - - try: - _call(args.module, args.action, args.arguments) - except Exception as exception: - logger.exception(exception) - sys.exit(1) - - -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 privileged module - if module_name not in privileged.__all__: - logger.critical('Bad module name: %r', module_name) - sys.exit(EXIT_SYNTAX) - - module = getattr(privileged, module_name) - try: - action = getattr(module, 'action_' + action_name) - except AttributeError: - logger.critical('Bad action: %s/%r', module_name, action_name) - sys.exit(EXIT_SYNTAX) - - for argument in arguments: - if not isinstance(argument, str): - logger.critical('Bad argument: %s', argument) - sys.exit(EXIT_SYNTAX) - - action(*arguments) - - -if __name__ == '__main__': - main() diff --git a/plinth/modules/email/__init__.py b/plinth/modules/email/__init__.py index 2f9236af0..76cb5fa3b 100644 --- a/plinth/modules/email/__init__.py +++ b/plinth/modules/email/__init__.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -FreedomBox app to manage an email server. -""" +"""FreedomBox app to manage an email server.""" import logging @@ -49,12 +47,13 @@ logger = logging.getLogger(__name__) class EmailApp(plinth.app.App): """FreedomBox app for an email server.""" + app_id = 'email' _version = 1 def __init__(self): - """The app's constructor""" + """Initialize the email app.""" super().__init__() info = plinth.app.Info(app_id=self.app_id, version=self._version, @@ -180,13 +179,14 @@ class EmailApp(plinth.app.App): super().setup(old_version) # Setup - privileged.home.setup() + privileged.setup_home() self.get_component('letsencrypt-email-postfix').setup_certificates() self.get_component('letsencrypt-email-dovecot').setup_certificates() - privileged.domain.set_domains() - privileged.postfix.setup() + privileged.domain.set_all_domains() + aliases.first_setup() + privileged.setup_postfix() aliases.setup_common_aliases(_get_first_admin()) - privileged.spam.setup() + privileged.setup_spam() # Restart daemons actions.superuser_run('service', ['try-restart', 'postfix']) @@ -218,7 +218,7 @@ def on_domain_added(sender, domain_type, name, description='', services=None, if app.needs_setup(): return - privileged.domain.set_domains() + privileged.domain.set_all_domains() def on_domain_removed(sender, domain_type, name='', **kwargs): @@ -227,4 +227,4 @@ def on_domain_removed(sender, domain_type, name='', **kwargs): if app.needs_setup(): return - privileged.domain.set_domains() + privileged.domain.set_all_domains() diff --git a/plinth/modules/email/aliases.py b/plinth/modules/email/aliases.py index 5d5b3fec8..425f63bd9 100644 --- a/plinth/modules/email/aliases.py +++ b/plinth/modules/email/aliases.py @@ -1,14 +1,12 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Manage email aliases stored in sqlite database. -""" +"""Manage email aliases stored in sqlite database.""" import contextlib import pwd import sqlite3 from dataclasses import dataclass -from plinth import actions +from . import privileged @dataclass @@ -75,7 +73,7 @@ def delete(username, aliases): def first_setup(): """Create the database file and schema inside it.""" - actions.superuser_run('email', ['aliases', 'setup']) + privileged.aliases.setup_aliases() # Create schema if not exists query = ''' diff --git a/plinth/modules/email/dns.py b/plinth/modules/email/dns.py index c6df85abe..fecd051b7 100644 --- a/plinth/modules/email/dns.py +++ b/plinth/modules/email/dns.py @@ -12,12 +12,11 @@ See: https://rspamd.com/doc/modules/dkim_signing.html from dataclasses import dataclass from typing import Union -from plinth.errors import ActionError - @dataclass class Entry: # pylint: disable=too-many-instance-attributes """A DNS entry.""" + type_: str value: str domain: Union[str, None] = None @@ -55,12 +54,12 @@ def get_entries(): f'rua=mailto:postmaster@{domain}; ') ] try: - dkim_public_key = privileged.dkim.get_public_key(domain) + dkim_public_key = privileged.get_dkim_public_key(domain) dkim_entries = [ Entry(domain='dkim._domainkey', type_='TXT', value=f'v=DKIM1; k=rsa; p={dkim_public_key}') ] - except ActionError: + except Exception: dkim_entries = [] autoconfig_entries = [ diff --git a/plinth/modules/email/privileged/__init__.py b/plinth/modules/email/privileged/__init__.py index 999f6efc0..cf5e8ea28 100644 --- a/plinth/modules/email/privileged/__init__.py +++ b/plinth/modules/email/privileged/__init__.py @@ -1,10 +1,14 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Provides privileged actions that run as root. -""" +"""Provides privileged actions that run as root.""" -from . import aliases, dkim, domain, home, postfix, spam, tls +from .aliases import setup_aliases +from .dkim import get_dkim_public_key, setup_dkim +from .domain import set_domains +from .home import setup_home +from .postfix import setup_postfix +from .spam import setup_spam -__all__ = ['aliases', 'domain', 'dkim', 'home', 'postfix', 'spam', 'tls'] - -from .aliases import action_setup +__all__ = [ + 'setup_aliases', 'get_dkim_public_key', 'setup_dkim', 'set_domains', + 'setup_home', 'setup_postfix', 'setup_spam' +] diff --git a/plinth/modules/email/privileged/aliases.py b/plinth/modules/email/privileged/aliases.py index 78e4ea62b..208a622b9 100644 --- a/plinth/modules/email/privileged/aliases.py +++ b/plinth/modules/email/privileged/aliases.py @@ -1,13 +1,14 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Privileged operations for managing aliases. -""" +"""Privileged operations for managing aliases.""" import pathlib import shutil +from plinth.actions import privileged -def action_setup(): + +@privileged +def setup_aliases(): """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) diff --git a/plinth/modules/email/privileged/dkim.py b/plinth/modules/email/privileged/dkim.py index 95a413e8e..f57ba47fb 100644 --- a/plinth/modules/email/privileged/dkim.py +++ b/plinth/modules/email/privileged/dkim.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Generate DKIM keys for signing outgoing messages. +"""Generate DKIM keys for signing outgoing messages. See: https://rspamd.com/doc/modules/dkim_signing.html """ @@ -10,7 +9,7 @@ import re import shutil import subprocess -from plinth import actions +from plinth.actions import privileged _keys_dir = pathlib.Path('/var/lib/rspamd/dkim/') @@ -23,24 +22,19 @@ def _validate_domain_name(domain): raise ValueError('Invalid domain name') -def get_public_key(domain): - """Return the DKIM public key for the given domain.""" - output = actions.superuser_run('email', - ['dkim', 'get_dkim_public_key', domain]) - return output.strip() - - -def action_get_dkim_public_key(domain): +@privileged +def get_dkim_public_key(domain: str) -> str: """Privileged action to get the public key from DKIM key.""" _validate_domain_name(domain) key_file = _keys_dir / f'{domain}.dkim.key' output = subprocess.check_output( ['openssl', 'rsa', '-in', str(key_file), '-pubout'], stderr=subprocess.DEVNULL) - print(''.join(output.decode().splitlines()[1:-1])) + return ''.join(output.decode().splitlines()[1:-1]) -def action_setup_dkim(domain): +@privileged +def setup_dkim(domain: str): """Create DKIM key for a given domain.""" _validate_domain_name(domain) diff --git a/plinth/modules/email/privileged/domain.py b/plinth/modules/email/privileged/domain.py index 10176933c..d8a792956 100644 --- a/plinth/modules/email/privileged/domain.py +++ b/plinth/modules/email/privileged/domain.py @@ -10,13 +10,13 @@ See: http://www.postfix.org/postconf.5.html#myhostname import pathlib import re -from plinth.actions import superuser_run +from plinth.actions import privileged from plinth.app import App from plinth.modules import config from plinth.modules.email import postfix from plinth.modules.names.components import DomainName -from . import tls +from . import dkim, tls def get_domains(): @@ -28,7 +28,7 @@ def get_domains(): return {'primary_domain': conf['mydomain'], 'all_domains': domains} -def set_domains(primary_domain=None): +def set_all_domains(primary_domain=None): """Set the primary domain and all the domains for postfix.""" all_domains = DomainName.list_names() if not primary_domain: @@ -37,10 +37,8 @@ def set_domains(primary_domain=None): primary_domain = config.get_domainname() or list(all_domains)[0] # Update configuration and don't restart daemons - superuser_run( - 'email', - ['domain', 'set_domains', primary_domain, ','.join(all_domains)]) - superuser_run('email', ['dkim', 'setup_dkim', primary_domain]) + set_domains(primary_domain, list(all_domains)) + dkim.setup_dkim(primary_domain) # Copy certificates (self-signed if needed) and restart daemons app = App.get('email') @@ -48,9 +46,10 @@ def set_domains(primary_domain=None): app.get_component('letsencrypt-email-dovecot').setup_certificates() -def action_set_domains(primary_domain, all_domains): +@privileged +def set_domains(primary_domain: str, all_domains: list[str]): """Set the primary domain and all the domains for postfix.""" - all_domains = [_clean_domain(domain) for domain in all_domains.split(',')] + all_domains = [_clean_domain(domain) for domain in all_domains] primary_domain = _clean_domain(primary_domain) defaults = {'$myhostname', 'localhost.$mydomain', 'localhost'} diff --git a/plinth/modules/email/privileged/home.py b/plinth/modules/email/privileged/home.py index 37d9ff2b4..544643dc3 100644 --- a/plinth/modules/email/privileged/home.py +++ b/plinth/modules/email/privileged/home.py @@ -8,20 +8,15 @@ https://doc.dovecot.org/configuration_manual/authentication/user_databases_userd import subprocess -from plinth import actions +from plinth.actions import privileged -def setup(): +@privileged +def setup_home(): """Set correct permissions on /var/mail/ directory. For each user, /var/mail/ 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/. - + directory. Ensure that 'others' can't access /var/mail/. """ - actions.superuser_run('email', ['home', 'setup']) - - -def action_setup(): - """Run chmod on /var/mail to remove all permissions for 'others'.""" subprocess.run(['chmod', 'o-rwx', '/var/mail'], check=True) diff --git a/plinth/modules/email/privileged/postfix.py b/plinth/modules/email/privileged/postfix.py index 9b42d6a50..be479c440 100644 --- a/plinth/modules/email/privileged/postfix.py +++ b/plinth/modules/email/privileged/postfix.py @@ -1,7 +1,8 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Configure postfix to use auth and local delivery with dovecot. Start smtps and -submission services. Setup aliases database. +"""Configure postix. + +- Configure postfix to use auth and local delivery with dovecot. +- Start SMTPS and submission services. Setup aliases database. See: https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/ @@ -9,9 +10,8 @@ See: https://doc.dovecot.org/configuration_manual/howto/postfix_dovecot_lmtp/ See: http://www.postfix.org/TLS_README.html """ -from plinth import actions +from plinth.actions import privileged -from .. import aliases from .. import postfix as postconf default_config = { @@ -57,13 +57,9 @@ smtps_service = postconf.Service(service='smtps', type_='inet', private='n', SQLITE_ALIASES = 'sqlite:/etc/postfix/freedombox-aliases.cf' -def setup(): - """Set SASL, mail submission, and user lookup settings.""" - aliases.first_setup() - actions.superuser_run('email', ['postfix', 'setup']) - - -def action_setup(): +@privileged +def setup_postfix(): + """Configure postfix.""" postconf.set_config(default_config) _setup_submission() _setup_alias_maps() diff --git a/plinth/modules/email/privileged/spam.py b/plinth/modules/email/privileged/spam.py index a2f8bbc8d..046a2b508 100644 --- a/plinth/modules/email/privileged/spam.py +++ b/plinth/modules/email/privileged/spam.py @@ -10,7 +10,7 @@ import pathlib import re import subprocess -from plinth import actions +from plinth.actions import privileged from plinth.modules.email import postfix _milter_config = { @@ -19,12 +19,8 @@ _milter_config = { } -def setup(): - """Trigger a privileged setup action.""" - actions.superuser_run('email', ['spam', 'setup']) - - -def action_setup(): +@privileged +def setup_spam(): """Compile sieve filters and set rspamd/postfix configuration.""" _compile_sieve() _setup_rspamd() diff --git a/plinth/modules/email/views.py b/plinth/modules/email/views.py index 70d8b4dd2..bac67c3b2 100644 --- a/plinth/modules/email/views.py +++ b/plinth/modules/email/views.py @@ -41,7 +41,7 @@ class EmailAppView(AppView): new_data = form.cleaned_data if old_data['primary_domain'] != new_data['primary_domain']: try: - privileged.domain.set_domains(new_data['primary_domain']) + privileged.domain.set_all_domains(new_data['primary_domain']) messages.success(self.request, _('Configuration updated')) except Exception: messages.error(self.request,