mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
Most modern setups simply use to PAM to lookup local recipients instead of integrating directly with LDAP. libnss-ldapd package that we install and configure connects the password database with LDAP. Anyone then using PAM need not be aware of LDAP integration. This reduces extra configuration and many problems that come along with it. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
193 lines
5.7 KiB
Python
193 lines
5.7 KiB
Python
"""Postconf wrapper providing thread-safe operations"""
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import logging
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from typing import ClassVar
|
|
|
|
from . import interproc
|
|
from .lock import Mutex
|
|
|
|
logger = logging.getLogger(__name__)
|
|
mutex = Mutex('email-postconf')
|
|
|
|
|
|
@dataclass
|
|
class ServiceFlags:
|
|
service: str
|
|
type: str
|
|
private: str
|
|
unpriv: str
|
|
chroot: str
|
|
wakeup: str
|
|
maxproc: str
|
|
command_args: str
|
|
|
|
crash_handler: ClassVar[str] = '/dev/null/plinth-crash'
|
|
|
|
def _get_flags_ordered(self):
|
|
return [self.service, self.type, self.private, self.unpriv,
|
|
self.chroot, self.wakeup, self.maxproc, self.command_args]
|
|
|
|
def serialize(self) -> str:
|
|
ordered = self._get_flags_ordered()
|
|
return ' '.join(ordered)
|
|
|
|
def serialize_temp(self) -> str:
|
|
ordered = self._get_flags_ordered()
|
|
ordered[-1] = self.crash_handler
|
|
return ' '.join(ordered)
|
|
|
|
def try_remove_crash_handler(self, line) -> str:
|
|
pattern = re.compile('([^ \\t]+)[ \\t]+([a-z]+)[ \\t]+')
|
|
match = pattern.match(line)
|
|
if match is None:
|
|
return None
|
|
if match.group(1) != self.service or match.group(2) != self.type:
|
|
return None
|
|
if not line.rstrip().endswith(self.crash_handler):
|
|
return None
|
|
return line.replace(self.crash_handler, self.command_args)
|
|
|
|
|
|
def get_many(key_list):
|
|
"""Acquire resource lock. Get the list of postconf values as specified.
|
|
Return a key-value map"""
|
|
for key in key_list:
|
|
validate_key(key)
|
|
with mutex.lock_all():
|
|
return get_many_unsafe(key_list)
|
|
|
|
|
|
def get_many_unsafe(key_iterator, flag=''):
|
|
result = {}
|
|
args = ['/sbin/postconf']
|
|
if flag:
|
|
args.append(flag)
|
|
|
|
number_of_keys = 0
|
|
for key in key_iterator:
|
|
args.append(key)
|
|
number_of_keys += 1
|
|
|
|
stdout = _run(args)
|
|
for line in filter(None, stdout.split('\n')):
|
|
key, sep, value = line.partition('=')
|
|
if not sep:
|
|
raise ValueError('Invalid output detected')
|
|
result[key.strip()] = value.strip()
|
|
|
|
if len(result) != number_of_keys:
|
|
raise ValueError('Some keys were missing from the output')
|
|
return result
|
|
|
|
|
|
def set_many(kv_map):
|
|
"""Acquire resource lock. Set the list of postconf values as specified"""
|
|
for key, value in kv_map.items():
|
|
validate_key(key)
|
|
validate_value(value)
|
|
|
|
with mutex.lock_all():
|
|
set_many_unsafe(kv_map)
|
|
|
|
|
|
def set_many_unsafe(kv_map, flag=''):
|
|
args = ['/sbin/postconf']
|
|
|
|
if not kv_map:
|
|
return
|
|
if flag:
|
|
args.append(flag)
|
|
for key, value in kv_map.items():
|
|
args.append('{}={}'.format(key, value))
|
|
_run(args)
|
|
|
|
|
|
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_key = service_flags.service + '/' + service_flags.type
|
|
long_opts = {service_key + '/' + k: v for (k, v) in options.items()}
|
|
|
|
logger.info('Setting %s service: %r', service_flags.service, options)
|
|
|
|
# Crash resistant config setting:
|
|
# /sbin/postconf -M "service/type=<temp flag string>"
|
|
# /sbin/postconf -P "service/type/k=v" ...
|
|
# Delete placeholder string /dev/null/plinth-crash
|
|
with mutex.lock_all():
|
|
set_unsafe(service_key, service_flags.serialize_temp(), '-M')
|
|
set_many_unsafe(long_opts, '-P')
|
|
_master_remove_crash_handler(service_flags)
|
|
|
|
|
|
def get_unsafe(key):
|
|
"""Get postconf value (no locking, no sanitization)"""
|
|
result = _run(['/sbin/postconf', key])
|
|
match = key + ' ='
|
|
if not result.startswith(match):
|
|
raise KeyError(key)
|
|
return result[len(match):].strip()
|
|
|
|
|
|
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 parse_maps(raw_value):
|
|
if '{' in raw_value or '}' in raw_value:
|
|
raise ValueError('Unsupported map list format')
|
|
|
|
value_list = []
|
|
for segment in raw_value.split(','):
|
|
for sub_segment in segment.strip().split(' '):
|
|
sub_segment = sub_segment.strip()
|
|
if sub_segment:
|
|
value_list.append(sub_segment)
|
|
return value_list
|
|
|
|
|
|
def _run(args):
|
|
"""Run process. Capture and return standard output as a string. Raise a
|
|
RuntimeError on non-zero exit codes"""
|
|
try:
|
|
result = subprocess.run(args, check=True, capture_output=True)
|
|
return result.stdout.decode('utf-8')
|
|
except subprocess.SubprocessError as subprocess_error:
|
|
raise RuntimeError('Subprocess failed') from subprocess_error
|
|
except UnicodeDecodeError as unicode_error:
|
|
raise RuntimeError('Unicode decoding failed') from unicode_error
|
|
|
|
|
|
def _master_remove_crash_handler(service_flags):
|
|
with interproc.atomically_rewrite('/etc/postfix/master.cf') as writer:
|
|
with open('/etc/postfix/master.cf') as reader:
|
|
for line in reader:
|
|
cleaned = service_flags.try_remove_crash_handler(line)
|
|
writer.write(line if cleaned is None else cleaned)
|
|
|
|
|
|
def validate_key(key):
|
|
"""Validate postconf key format. Raises ValueError"""
|
|
if not re.match('^[a-zA-Z][a-zA-Z0-9_]*$', key):
|
|
raise ValueError('Invalid postconf key format')
|
|
|
|
|
|
def validate_value(value):
|
|
"""Validate postconf value format. Raises ValueError"""
|
|
for c in value:
|
|
if ord(c) < 32:
|
|
raise ValueError('Value contains control characters')
|