appsv2/mail: add new 'allow_email' flag on app system users that will autogenerate a passwd-like file to be used by dovecot + map for postfix

This commit is contained in:
Alexandre Aubin 2023-02-28 17:56:49 +01:00
parent f0751aff17
commit c48d9ec483
6 changed files with 88 additions and 10 deletions

View file

@ -44,20 +44,20 @@ passdb {
driver = ldap driver = ldap
} }
# Internally, allow authentication from system user # Internally, allow authentication from apps system user who have "enable_email = true"
# who might want to send emails (e.g. from apps)
passdb { passdb {
override_fields = allow_nets=127.0.0.1/32,::1/64 driver = passwd-file
driver = pam args = /etc/dovecot/app-senders-passwd
} }
userdb { userdb {
args = /etc/dovecot/dovecot-ldap.conf
driver = ldap driver = ldap
args = /etc/dovecot/dovecot-ldap.conf
} }
userdb { userdb {
driver = passwd driver = passwd-file
args = /etc/dovecot/app-senders-passwd
} }
protocol imap { protocol imap {

View file

@ -108,8 +108,10 @@ virtual_minimum_uid = 100
virtual_uid_maps = static:vmail virtual_uid_maps = static:vmail
virtual_gid_maps = static:mail virtual_gid_maps = static:mail
smtpd_sender_login_maps= smtpd_sender_login_maps=
ldap:/etc/postfix/ldap-accounts.cf, # Regular Yunohost accounts # Regular Yunohost accounts
hash:/etc/postfix/sender_login_maps # Extra maps for system users who need to send emails ldap:/etc/postfix/ldap-accounts.cf,
# Extra maps for app system users who need to send emails
hash:/etc/postfix/app_senders_login_maps
# Dovecot LDA # Dovecot LDA

View file

@ -80,6 +80,8 @@ do_post_regen() {
postmap -F hash:/etc/postfix/sni postmap -F hash:/etc/postfix/sni
python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="postfix")'
[[ -z "$regen_conf_files" ]] \ [[ -z "$regen_conf_files" ]] \
|| { systemctl restart postfix && systemctl restart postsrsd; } || { systemctl restart postfix && systemctl restart postsrsd; }

View file

@ -53,6 +53,8 @@ do_post_regen() {
chown root:mail /var/mail chown root:mail /var/mail
chmod 1775 /var/mail chmod 1775 /var/mail
python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="dovecot")'
[ -z "$regen_conf_files" ] && exit 0 [ -z "$regen_conf_files" ] && exit 0
# compile sieve script # compile sieve script

View file

@ -1582,6 +1582,9 @@ def app_setting(app, key, value=None, delete=False):
if delete: if delete:
if key in app_settings: if key in app_settings:
del app_settings[key] del app_settings[key]
else:
# Don't call _set_app_settings to avoid unecessary writes...
return
# SET # SET
else: else:
@ -3148,3 +3151,45 @@ def _ask_confirmation(
if not answer: if not answer:
raise YunohostError("aborting") raise YunohostError("aborting")
def regen_mail_app_user_config_for_dovecot_and_postfix(only=None):
dovecot = True if only in [None, "dovecot"] else False
postfix = True if only in [None, "postfix"] else False
from yunohost.user import _hash_user_password
postfix_map = []
dovecot_passwd = []
for app in _installed_apps():
settings = _get_app_settings(app)
if "domain" not in settings or "mail_pwd" not in settings:
continue
if dovecot:
hashed_password = _hash_user_password(settings["mail_pwd"])
dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24")
if postfix:
postfix_map.append(f"{app}@{settings['domain']} {app}")
if dovecot:
app_senders_passwd = "/etc/dovecot/app-senders-passwd"
content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!"
content += '\n' + '\n'.join(dovecot_passwd)
write_to_file(app_senders_passwd, content)
chmod(app_senders_passwd, 0o440)
chown(app_senders_passwd, "root", "dovecot")
if postfix:
app_senders_map = "/etc/postfix/app_senders_login_maps"
content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!"
content += '\n' + '\n'.join(postfix_map)
write_to_file(app_senders_map, content)
chmod(app_senders_map, 0o440)
chown(app_senders_map, "postfix", "root")
os.system(f"postmap {app_senders_map} 2>/dev/null")
chmod(app_senders_map + ".db", 0o640)
chown(app_senders_map + ".db", "postfix", "root")

View file

@ -24,6 +24,7 @@ import tempfile
from typing import Dict, Any, List from typing import Dict, Any, List
from moulinette import m18n from moulinette import m18n
from moulinette.utils.text import random_ascii
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file
@ -473,6 +474,7 @@ class SystemuserAppResource(AppResource):
default_properties: Dict[str, Any] = { default_properties: Dict[str, Any] = {
"allow_ssh": False, "allow_ssh": False,
"allow_sftp": False, "allow_sftp": False,
"allow_email": False,
"home": "/var/www/__APP__", "home": "/var/www/__APP__",
} }
@ -480,9 +482,13 @@ class SystemuserAppResource(AppResource):
allow_ssh: bool = False allow_ssh: bool = False
allow_sftp: bool = False allow_sftp: bool = False
allow_email: bool = False
home: str = "" home: str = ""
def provision_or_update(self, context: Dict = {}): def provision_or_update(self, context: Dict = {}):
from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix
# FIXME : validate that no yunohost user exists with that name? # FIXME : validate that no yunohost user exists with that name?
# and/or that no system user exists during install ? # and/or that no system user exists during install ?
@ -527,7 +533,25 @@ class SystemuserAppResource(AppResource):
f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd"
) )
# Update mail-related stuff
if self.allow_email:
mail_pwd = self.get_setting("mail_pwd")
if not mail_pwd:
mail_pwd = random_ascii(24)
self.set_setting("mail_pwd", mail_pwd)
regen_mail_app_user_config_for_dovecot_and_postfix()
else:
self.delete_setting("mail_pwd")
if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \
or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0:
regen_mail_app_user_config_for_dovecot_and_postfix()
def deprovision(self, context: Dict = {}): def deprovision(self, context: Dict = {}):
from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix
if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0:
os.system(f"deluser {self.app} >/dev/null") os.system(f"deluser {self.app} >/dev/null")
if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0:
@ -542,6 +566,11 @@ class SystemuserAppResource(AppResource):
f"Failed to delete system user for {self.app}", raw_msg=True f"Failed to delete system user for {self.app}", raw_msg=True
) )
self.delete_setting("mail_pwd")
if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \
or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0:
regen_mail_app_user_config_for_dovecot_and_postfix()
# FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands...
@ -1060,8 +1089,6 @@ class DatabaseAppResource(AppResource):
self.set_setting("db_pwd", db_pwd) self.set_setting("db_pwd", db_pwd)
if not db_pwd: if not db_pwd:
from moulinette.utils.text import random_ascii
db_pwd = random_ascii(24) db_pwd = random_ascii(24)
self.set_setting("db_pwd", db_pwd) self.set_setting("db_pwd", db_pwd)