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:
fliu 2021-06-17 00:57:46 +00:00 committed by Sunil Mohan Adapa
parent cde0b47064
commit fdc6f23908
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
5 changed files with 112 additions and 67 deletions

View File

@ -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__':

View File

@ -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

View File

@ -6,7 +6,7 @@ from . import models
def get():
# Stub
return models.Result('Email domains')
return [models.Result('Email domains')]
def repair():

View File

@ -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)

View File

@ -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):