mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-02-18 08:33:41 +00:00
email: Implement email_server ipc set_sasl and set_submission
- Rewrote action script to eliminate stdin communication - Changed return type of audit.*.get() - An audit can return multiple lines of diagnostics - Move recommended endpoint URLs into function docstrings
This commit is contained in:
parent
cde0b47064
commit
fdc6f23908
@ -3,15 +3,23 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import plinth.modules.email_server.postconf as postconf
|
||||
import plinth.modules.email_server.audit as audit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXIT_SYNTAX = 10
|
||||
EXIT_TIMEOUT = 20
|
||||
EXIT_PERM = 20
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def main():
|
||||
@ -24,43 +32,14 @@ def main():
|
||||
globals()[function_name]()
|
||||
|
||||
|
||||
def ipc_postconf_set_many_v1():
|
||||
"""Set postconf values"""
|
||||
if os.getuid() != 0:
|
||||
logger.warning('Run as root?')
|
||||
new_config = _postconf_parse_stdin()
|
||||
with postconf.postconf_mutex.lock_all():
|
||||
for key, value in new_config.items():
|
||||
postconf.set_no_lock_assuming_root(key, value)
|
||||
@reserved_for_root
|
||||
def ipc_set_sasl():
|
||||
audit.ldap.action_set_sasl()
|
||||
|
||||
|
||||
def _postconf_parse_stdin():
|
||||
new_config = {}
|
||||
# Set timeout handler
|
||||
signal.signal(signal.SIGALRM, _timeout_handler)
|
||||
while True:
|
||||
key = _timed_input('Key: ')
|
||||
if not key: break
|
||||
postconf.validate_key(key)
|
||||
value = _timed_input('Value: ')
|
||||
postconf.validate_value(value)
|
||||
new_config[key] = value
|
||||
return new_config
|
||||
|
||||
|
||||
def _timed_input(prompt):
|
||||
# Set timeout
|
||||
signal.alarm(3)
|
||||
result = input(prompt)
|
||||
# Disable timeout
|
||||
signal.alarm(0)
|
||||
return result
|
||||
|
||||
|
||||
def _timeout_handler(signum, frame):
|
||||
# https://stackoverflow.com/a/1336751
|
||||
logger.critical('[Time out]')
|
||||
sys.exit(EXIT_TIMEOUT)
|
||||
@reserved_for_root
|
||||
def ipc_set_submission():
|
||||
audit.ldap.action_set_submission()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -13,7 +13,7 @@ from plinth.modules.firewall.components import Firewall
|
||||
from . import audit
|
||||
from . import manifest
|
||||
|
||||
version = 1
|
||||
version = 31
|
||||
managed_packages = ['postfix', 'dovecot-pop3d', 'dovecot-imapd',
|
||||
'dovecot-lmtpd', 'dovecot-ldap', 'dovecot-managesieved']
|
||||
managed_services = ['postfix', 'dovecot']
|
||||
@ -86,8 +86,8 @@ class EmailServerApp(plinth.app.App):
|
||||
def diagnose(self):
|
||||
"""Run diagnostics and return the results"""
|
||||
results = super().diagnose()
|
||||
results.append(audit.domain.get().summarize())
|
||||
results.append(audit.ldap.get().summarize())
|
||||
results.extend([r.summarize() for r in audit.domain.get()])
|
||||
results.extend([r.summarize() for r in audit.ldap.get()])
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ from . import models
|
||||
|
||||
def get():
|
||||
# Stub
|
||||
return models.Result('Email domains')
|
||||
return [models.Result('Email domains')]
|
||||
|
||||
|
||||
def repair():
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
"""Audit of LDAP and mail submission settings"""
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from plinth import actions
|
||||
|
||||
import plinth.modules.email_server.postconf as postconf
|
||||
from . import models
|
||||
|
||||
@ -8,18 +12,49 @@ default_config = {
|
||||
'smtpd_sasl_path': 'private/auth'
|
||||
}
|
||||
|
||||
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',
|
||||
'smtpd_tls_security_level': 'encrypt',
|
||||
'smtpd_client_restrictions': 'permit_sasl_authenticated,reject',
|
||||
'smtpd_relay_restrictions': 'permit_sasl_authenticated,reject'
|
||||
}
|
||||
|
||||
|
||||
# GET /audit/domain
|
||||
def get():
|
||||
"""Compare current values with the default. Generate an audit report"""
|
||||
results = models.Result('LDAP for user accounts')
|
||||
"""Compare current values with the default. Generate an audit report
|
||||
|
||||
Recommended endpoint name:
|
||||
GET /audit/ldap
|
||||
"""
|
||||
results = models.Result('Postfix uses Dovecot for SASL authentication')
|
||||
current_config = postconf.get_many(list(default_config.keys()))
|
||||
for key, value in default_config.items():
|
||||
if current_config[key] != value:
|
||||
results.fails.append('{} should equal {}'.format(key, value))
|
||||
return results
|
||||
return [results]
|
||||
|
||||
|
||||
# POST /audit/domain/repair
|
||||
def repair():
|
||||
"""Tries to repair LDAP and mail submission settings
|
||||
|
||||
Recommended endpoint name:
|
||||
POST /audit/ldap/repair
|
||||
"""
|
||||
actions.superuser_run('email_server', ['ipc', 'set_sasl'])
|
||||
actions.superuser_run('email_server', ['ipc', 'set_submission'])
|
||||
|
||||
|
||||
def action_set_sasl():
|
||||
"""Called by email_server ipc set_sasl"""
|
||||
postconf.set_many(default_config)
|
||||
|
||||
|
||||
def action_set_submission():
|
||||
"""Called by email_server ipc set_submission"""
|
||||
postconf.set_master_cf_options(service_flags=submission_flags,
|
||||
options=default_submission_options)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""Postconf wrapper providing thread-safe operations"""
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import subprocess
|
||||
import plinth.actions
|
||||
@ -9,38 +10,67 @@ from .lock import Mutex
|
||||
postconf_mutex = Mutex('plinth-email-postconf.lock')
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ServiceFlags:
|
||||
service: str
|
||||
type: str
|
||||
private: str
|
||||
unpriv: str
|
||||
chroot: str
|
||||
wakeup: str
|
||||
maxproc: str
|
||||
command_args: str
|
||||
|
||||
def serialize(self) -> str:
|
||||
return ' '.join([self.service, self.type, self.private, self.unpriv,
|
||||
self.chroot, self.wakeup, self.maxproc,
|
||||
self.command_args])
|
||||
|
||||
|
||||
def get_many(key_list):
|
||||
"""Acquire resource lock. Get the list of postconf values as specified.
|
||||
Return a key-value map"""
|
||||
result = {}
|
||||
for key in key_list:
|
||||
validate_key(key)
|
||||
with postconf_mutex.lock_all():
|
||||
for key in key_list:
|
||||
result[key] = get_no_lock(key)
|
||||
result[key] = get_unsafe(key)
|
||||
return result
|
||||
|
||||
|
||||
def set_many(kv_map):
|
||||
"""Acquire resource lock. Set the list of postconf values as specified"""
|
||||
# Encode email_server ipc input
|
||||
lines = []
|
||||
for key, value in kv_map.items():
|
||||
validate_key(key)
|
||||
validate_value(value)
|
||||
lines.append(key)
|
||||
lines.append(value)
|
||||
lines.append('\n')
|
||||
stdin = '\n'.join(lines).encode('utf-8')
|
||||
|
||||
# Run action script as root
|
||||
args = ['ipc', 'postconf_set_many_v1']
|
||||
with postconf_mutex.lock_threads_only():
|
||||
# The action script will take care of file locking
|
||||
plinth.actions.superuser_run('email_server', args, input=stdin)
|
||||
with postconf_mutex.lock_all():
|
||||
for key, value in kv_map.items():
|
||||
set_unsafe(key, value)
|
||||
|
||||
|
||||
def get_no_lock(key):
|
||||
"""Get postconf value (no locking)"""
|
||||
validate_key(key)
|
||||
def set_master_cf_options(service_flags, options):
|
||||
"""Acquire resource lock. Set master.cf service options"""
|
||||
if not isinstance(service_flags, ServiceFlags):
|
||||
raise TypeError('service_flags')
|
||||
for key, value in options.items():
|
||||
validate_key(key)
|
||||
validate_value(value)
|
||||
|
||||
service_slash_type = service_flags.service + '/' + service_flags.type
|
||||
flag_string = service_flags.serialize()
|
||||
|
||||
with postconf_mutex.lock_all():
|
||||
# /sbin/postconf -M "service/type=flag_string"
|
||||
set_unsafe(service_slash_type, flag_string, '-M')
|
||||
for short_key, value in options.items():
|
||||
# /sbin/postconf -P "service/type/short_key=value"
|
||||
set_unsafe(service_slash_type + '/' + short_key, value, '-P')
|
||||
|
||||
|
||||
def get_unsafe(key):
|
||||
"""Get postconf value (no locking, no sanitization)"""
|
||||
result = _run(['/sbin/postconf', key])
|
||||
match = key + ' = '
|
||||
if not result.startswith(match):
|
||||
@ -48,11 +78,12 @@ def get_no_lock(key):
|
||||
return result[len(match):].strip()
|
||||
|
||||
|
||||
def set_no_lock_assuming_root(key, value):
|
||||
"""Set postconf value (assuming root and no locking)"""
|
||||
validate_key(key)
|
||||
validate_value(value)
|
||||
_run(['/sbin/postconf', '{}={}'.format(key, value)])
|
||||
def set_unsafe(key, value, flag=''):
|
||||
"""Set postconf value (assuming root, no locking, no sanitization)"""
|
||||
if flag:
|
||||
_run(['/sbin/postconf', flag, '{}={}'.format(key, value)])
|
||||
else:
|
||||
_run(['/sbin/postconf', '{}={}'.format(key, value)])
|
||||
|
||||
|
||||
def _run(args):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user