mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-07-22 11:59:33 +00:00
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 <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
a579c648fd
commit
5389303e98
@ -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()
|
||||
@ -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()
|
||||
|
||||
@ -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 = '''
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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'
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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/<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/.
|
||||
|
||||
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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user