mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +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 logging
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import plinth.modules.email_server.postconf as postconf
|
import plinth.modules.email_server.audit as audit
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
EXIT_SYNTAX = 10
|
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():
|
def main():
|
||||||
@ -24,43 +32,14 @@ def main():
|
|||||||
globals()[function_name]()
|
globals()[function_name]()
|
||||||
|
|
||||||
|
|
||||||
def ipc_postconf_set_many_v1():
|
@reserved_for_root
|
||||||
"""Set postconf values"""
|
def ipc_set_sasl():
|
||||||
if os.getuid() != 0:
|
audit.ldap.action_set_sasl()
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def _postconf_parse_stdin():
|
@reserved_for_root
|
||||||
new_config = {}
|
def ipc_set_submission():
|
||||||
# Set timeout handler
|
audit.ldap.action_set_submission()
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from plinth.modules.firewall.components import Firewall
|
|||||||
from . import audit
|
from . import audit
|
||||||
from . import manifest
|
from . import manifest
|
||||||
|
|
||||||
version = 1
|
version = 31
|
||||||
managed_packages = ['postfix', 'dovecot-pop3d', 'dovecot-imapd',
|
managed_packages = ['postfix', 'dovecot-pop3d', 'dovecot-imapd',
|
||||||
'dovecot-lmtpd', 'dovecot-ldap', 'dovecot-managesieved']
|
'dovecot-lmtpd', 'dovecot-ldap', 'dovecot-managesieved']
|
||||||
managed_services = ['postfix', 'dovecot']
|
managed_services = ['postfix', 'dovecot']
|
||||||
@ -86,8 +86,8 @@ class EmailServerApp(plinth.app.App):
|
|||||||
def diagnose(self):
|
def diagnose(self):
|
||||||
"""Run diagnostics and return the results"""
|
"""Run diagnostics and return the results"""
|
||||||
results = super().diagnose()
|
results = super().diagnose()
|
||||||
results.append(audit.domain.get().summarize())
|
results.extend([r.summarize() for r in audit.domain.get()])
|
||||||
results.append(audit.ldap.get().summarize())
|
results.extend([r.summarize() for r in audit.ldap.get()])
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from . import models
|
|||||||
|
|
||||||
def get():
|
def get():
|
||||||
# Stub
|
# Stub
|
||||||
return models.Result('Email domains')
|
return [models.Result('Email domains')]
|
||||||
|
|
||||||
|
|
||||||
def repair():
|
def repair():
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
|
"""Audit of LDAP and mail submission settings"""
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
from plinth import actions
|
||||||
|
|
||||||
import plinth.modules.email_server.postconf as postconf
|
import plinth.modules.email_server.postconf as postconf
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
@ -8,18 +12,49 @@ default_config = {
|
|||||||
'smtpd_sasl_path': 'private/auth'
|
'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():
|
def get():
|
||||||
"""Compare current values with the default. Generate an audit report"""
|
"""Compare current values with the default. Generate an audit report
|
||||||
results = models.Result('LDAP for user accounts')
|
|
||||||
|
Recommended endpoint name:
|
||||||
|
GET /audit/ldap
|
||||||
|
"""
|
||||||
|
results = models.Result('Postfix uses Dovecot for SASL authentication')
|
||||||
current_config = postconf.get_many(list(default_config.keys()))
|
current_config = postconf.get_many(list(default_config.keys()))
|
||||||
for key, value in default_config.items():
|
for key, value in default_config.items():
|
||||||
if current_config[key] != value:
|
if current_config[key] != value:
|
||||||
results.fails.append('{} should equal {}'.format(key, value))
|
results.fails.append('{} should equal {}'.format(key, value))
|
||||||
return results
|
return [results]
|
||||||
|
|
||||||
|
|
||||||
# POST /audit/domain/repair
|
|
||||||
def 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)
|
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"""
|
"""Postconf wrapper providing thread-safe operations"""
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import plinth.actions
|
import plinth.actions
|
||||||
@ -9,38 +10,67 @@ from .lock import Mutex
|
|||||||
postconf_mutex = Mutex('plinth-email-postconf.lock')
|
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):
|
def get_many(key_list):
|
||||||
"""Acquire resource lock. Get the list of postconf values as specified.
|
"""Acquire resource lock. Get the list of postconf values as specified.
|
||||||
Return a key-value map"""
|
Return a key-value map"""
|
||||||
result = {}
|
result = {}
|
||||||
|
for key in key_list:
|
||||||
|
validate_key(key)
|
||||||
with postconf_mutex.lock_all():
|
with postconf_mutex.lock_all():
|
||||||
for key in key_list:
|
for key in key_list:
|
||||||
result[key] = get_no_lock(key)
|
result[key] = get_unsafe(key)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def set_many(kv_map):
|
def set_many(kv_map):
|
||||||
"""Acquire resource lock. Set the list of postconf values as specified"""
|
"""Acquire resource lock. Set the list of postconf values as specified"""
|
||||||
# Encode email_server ipc input
|
|
||||||
lines = []
|
|
||||||
for key, value in kv_map.items():
|
for key, value in kv_map.items():
|
||||||
validate_key(key)
|
validate_key(key)
|
||||||
validate_value(value)
|
validate_value(value)
|
||||||
lines.append(key)
|
|
||||||
lines.append(value)
|
|
||||||
lines.append('\n')
|
|
||||||
stdin = '\n'.join(lines).encode('utf-8')
|
|
||||||
|
|
||||||
# Run action script as root
|
with postconf_mutex.lock_all():
|
||||||
args = ['ipc', 'postconf_set_many_v1']
|
for key, value in kv_map.items():
|
||||||
with postconf_mutex.lock_threads_only():
|
set_unsafe(key, value)
|
||||||
# The action script will take care of file locking
|
|
||||||
plinth.actions.superuser_run('email_server', args, input=stdin)
|
|
||||||
|
|
||||||
|
|
||||||
def get_no_lock(key):
|
def set_master_cf_options(service_flags, options):
|
||||||
"""Get postconf value (no locking)"""
|
"""Acquire resource lock. Set master.cf service options"""
|
||||||
validate_key(key)
|
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])
|
result = _run(['/sbin/postconf', key])
|
||||||
match = key + ' = '
|
match = key + ' = '
|
||||||
if not result.startswith(match):
|
if not result.startswith(match):
|
||||||
@ -48,11 +78,12 @@ def get_no_lock(key):
|
|||||||
return result[len(match):].strip()
|
return result[len(match):].strip()
|
||||||
|
|
||||||
|
|
||||||
def set_no_lock_assuming_root(key, value):
|
def set_unsafe(key, value, flag=''):
|
||||||
"""Set postconf value (assuming root and no locking)"""
|
"""Set postconf value (assuming root, no locking, no sanitization)"""
|
||||||
validate_key(key)
|
if flag:
|
||||||
validate_value(value)
|
_run(['/sbin/postconf', flag, '{}={}'.format(key, value)])
|
||||||
_run(['/sbin/postconf', '{}={}'.format(key, value)])
|
else:
|
||||||
|
_run(['/sbin/postconf', '{}={}'.format(key, value)])
|
||||||
|
|
||||||
|
|
||||||
def _run(args):
|
def _run(args):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user