mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
email_server: Don't use user IDs when performing lookups
- Typical mail systems are configured to work on usernames or virtual usernames. UIDs/GIDs are only needed at the final moment when delivering mails to user inboxes that need to have proper UID/GID set. - This makes it easy for dovecot to simply use PAM authentication instead of having to use LDAP. - Trying to hide UID from email headers is no longer necessary. Received: header is important for debugging mail delivery across the chain. Don't miss out. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
fb47f35e87
commit
d0cf01fb29
@ -11,7 +11,7 @@ from plinth import actions
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Alias:
|
class Alias:
|
||||||
value: int
|
value: str
|
||||||
name: str
|
name: str
|
||||||
enabled: bool = field(init=False)
|
enabled: bool = field(init=False)
|
||||||
status: InitVar[int]
|
status: InitVar[int]
|
||||||
@ -34,11 +34,11 @@ def _get_cursor():
|
|||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
def get(uid):
|
def get(username):
|
||||||
"""Get all aliases of a user."""
|
"""Get all aliases of a user."""
|
||||||
query = 'SELECT name, value, status FROM alias WHERE value=?'
|
query = 'SELECT name, value, status FROM alias WHERE value=?'
|
||||||
with _get_cursor() as cursor:
|
with _get_cursor() as cursor:
|
||||||
rows = cursor.execute(query, (uid, ))
|
rows = cursor.execute(query, (username, ))
|
||||||
return [Alias(**row) for row in rows]
|
return [Alias(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
@ -56,40 +56,40 @@ def exists(name):
|
|||||||
return cursor.fetchone()[0] != 0
|
return cursor.fetchone()[0] != 0
|
||||||
|
|
||||||
|
|
||||||
def put(uid, name):
|
def put(username, name):
|
||||||
"""Insert if not exists a new alias."""
|
"""Insert if not exists a new alias."""
|
||||||
query = 'INSERT INTO alias (name, value, status) VALUES (?, ?, ?)'
|
query = 'INSERT INTO alias (name, value, status) VALUES (?, ?, ?)'
|
||||||
with _get_cursor() as cursor:
|
with _get_cursor() as cursor:
|
||||||
try:
|
try:
|
||||||
cursor.execute(query, (name, uid, 1))
|
cursor.execute(query, (name, username, 1))
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
pass # Alias exists, rare since we are already checking
|
pass # Alias exists, rare since we are already checking
|
||||||
|
|
||||||
|
|
||||||
def delete(uid, aliases):
|
def delete(username, aliases):
|
||||||
"""Delete a set of aliases."""
|
"""Delete a set of aliases."""
|
||||||
query = 'DELETE FROM alias WHERE value=? AND name=?'
|
query = 'DELETE FROM alias WHERE value=? AND name=?'
|
||||||
parameter_seq = ((uid, name) for name in aliases)
|
parameter_seq = ((username, name) for name in aliases)
|
||||||
with _get_cursor() as cursor:
|
with _get_cursor() as cursor:
|
||||||
cursor.execute('BEGIN')
|
cursor.execute('BEGIN')
|
||||||
cursor.executemany(query, parameter_seq)
|
cursor.executemany(query, parameter_seq)
|
||||||
cursor.execute('COMMIT')
|
cursor.execute('COMMIT')
|
||||||
|
|
||||||
|
|
||||||
def enable(uid, aliases):
|
def enable(username, aliases):
|
||||||
"""Enable a list of aliases."""
|
"""Enable a list of aliases."""
|
||||||
return _set_status(uid, aliases, 1)
|
return _set_status(username, aliases, 1)
|
||||||
|
|
||||||
|
|
||||||
def disable(uid, aliases):
|
def disable(username, aliases):
|
||||||
"""Disable a list of aliases."""
|
"""Disable a list of aliases."""
|
||||||
return _set_status(uid, aliases, 0)
|
return _set_status(username, aliases, 0)
|
||||||
|
|
||||||
|
|
||||||
def _set_status(uid, aliases, status):
|
def _set_status(username, aliases, status):
|
||||||
"""Set the status value of a list of aliases."""
|
"""Set the status value of a list of aliases."""
|
||||||
query = 'UPDATE alias SET status=? WHERE value=? AND name=?'
|
query = 'UPDATE alias SET status=? WHERE value=? AND name=?'
|
||||||
parameter_seq = ((status, uid, name) for name in aliases)
|
parameter_seq = ((status, username, name) for name in aliases)
|
||||||
with _get_cursor() as cursor:
|
with _get_cursor() as cursor:
|
||||||
cursor.execute('BEGIN')
|
cursor.execute('BEGIN')
|
||||||
cursor.executemany(query, parameter_seq)
|
cursor.executemany(query, parameter_seq)
|
||||||
@ -106,7 +106,7 @@ PRAGMA journal_mode=WAL;
|
|||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE TABLE IF NOT EXISTS alias (
|
CREATE TABLE IF NOT EXISTS alias (
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
value INTEGER NOT NULL,
|
value TEXT NOT NULL,
|
||||||
status INTEGER NOT NULL,
|
status INTEGER NOT NULL,
|
||||||
PRIMARY KEY (name)
|
PRIMARY KEY (name)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -54,9 +54,7 @@ default_smtps_options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MAILSRV_DIR = '/var/lib/plinth/mailsrv'
|
MAILSRV_DIR = '/var/lib/plinth/mailsrv'
|
||||||
ETC_ALIASES = 'hash:/etc/aliases'
|
SQLITE_ALIASES = 'sqlite:/etc/postfix/freedombox-aliases.cf'
|
||||||
BEFORE_ALIASES = 'ldap:/etc/postfix/freedombox-username-to-uid-number.cf'
|
|
||||||
AFTER_ALIASES = 'sqlite:/etc/postfix/freedombox-aliases.cf'
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -123,45 +121,26 @@ def check_alias_maps(title=''):
|
|||||||
"""Check the ability to mail to usernames and user aliases"""
|
"""Check the ability to mail to usernames and user aliases"""
|
||||||
diagnosis = models.MainCfDiagnosis(title)
|
diagnosis = models.MainCfDiagnosis(title)
|
||||||
|
|
||||||
analysis = models.AliasMapsAnalysis()
|
alias_maps = postconf.get_unsafe('alias_maps').replace(',', ' ').split(' ')
|
||||||
analysis.parsed = postconf.parse_maps_by_key_unsafe('alias_maps')
|
if SQLITE_ALIASES not in alias_maps:
|
||||||
analysis.isystem = list_find(analysis.parsed, ETC_ALIASES)
|
diagnosis.flag_once('alias_maps', user=alias_maps)
|
||||||
analysis.ibefore = list_find(analysis.parsed, BEFORE_ALIASES)
|
|
||||||
analysis.iafter = list_find(analysis.parsed, AFTER_ALIASES)
|
|
||||||
|
|
||||||
if analysis.ibefore == -1 or analysis.iafter == -1:
|
|
||||||
diagnosis.flag_once('alias_maps', user=analysis)
|
|
||||||
diagnosis.critical('Required maps not in list')
|
diagnosis.critical('Required maps not in list')
|
||||||
if analysis.ibefore > analysis.iafter:
|
|
||||||
diagnosis.flag_once('alias_maps', user=analysis)
|
|
||||||
diagnosis.critical('Insecure map order')
|
|
||||||
|
|
||||||
return diagnosis
|
return diagnosis
|
||||||
|
|
||||||
|
|
||||||
def fix_alias_maps(diagnosis):
|
def fix_alias_maps(diagnosis):
|
||||||
diagnosis.repair('alias_maps', rearrange_alias_maps)
|
|
||||||
|
def fix_value(alias_maps):
|
||||||
|
if SQLITE_ALIASES not in alias_maps:
|
||||||
|
alias_maps.append(SQLITE_ALIASES)
|
||||||
|
|
||||||
|
return ' '.join(alias_maps)
|
||||||
|
|
||||||
|
diagnosis.repair('alias_maps', fix_value)
|
||||||
diagnosis.apply_changes(postconf.set_many_unsafe)
|
diagnosis.apply_changes(postconf.set_many_unsafe)
|
||||||
|
|
||||||
|
|
||||||
def rearrange_alias_maps(analysis):
|
|
||||||
# Delete *all* references to BEFORE_ALIASES and AFTER_ALIASES
|
|
||||||
for i in range(len(analysis.parsed)):
|
|
||||||
if analysis.parsed[i] in (BEFORE_ALIASES, AFTER_ALIASES):
|
|
||||||
analysis.parsed[i] = ''
|
|
||||||
# Does hash:/etc/aliases exist in list?
|
|
||||||
if analysis.isystem >= 0:
|
|
||||||
# Put the maps around hash:/etc/aliases
|
|
||||||
val = '%s %s %s' % (BEFORE_ALIASES, ETC_ALIASES, AFTER_ALIASES)
|
|
||||||
analysis.parsed[analysis.isystem] = val
|
|
||||||
else:
|
|
||||||
# To the end
|
|
||||||
analysis.parsed.append(BEFORE_ALIASES)
|
|
||||||
analysis.parsed.append(AFTER_ALIASES)
|
|
||||||
# List -> string
|
|
||||||
return ' '.join(filter(None, analysis.parsed))
|
|
||||||
|
|
||||||
|
|
||||||
def check_local_recipient_maps(title=''):
|
def check_local_recipient_maps(title=''):
|
||||||
diagnosis = models.MainCfDiagnosis(title)
|
diagnosis = models.MainCfDiagnosis(title)
|
||||||
lrcpt_maps = postconf.parse_maps_by_key_unsafe('local_recipient_maps')
|
lrcpt_maps = postconf.parse_maps_by_key_unsafe('local_recipient_maps')
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Models of the audit module"""
|
"""Models of the audit module"""
|
||||||
|
|
||||||
import dataclasses
|
|
||||||
import logging
|
import logging
|
||||||
import typing
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -207,14 +205,3 @@ class MainCfDiagnosis(Diagnosis):
|
|||||||
contains an unresolved issue (i.e. an uncorrected key)"""
|
contains an unresolved issue (i.e. an uncorrected key)"""
|
||||||
if None in self.advice.values():
|
if None in self.advice.values():
|
||||||
raise UnresolvedIssueError('Assertion failed')
|
raise UnresolvedIssueError('Assertion failed')
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(init=False)
|
|
||||||
class AliasMapsAnalysis:
|
|
||||||
parsed = typing.List[str]
|
|
||||||
ibefore = int
|
|
||||||
isystem = int
|
|
||||||
iafter = int
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|||||||
@ -12,15 +12,6 @@ passdb {
|
|||||||
result_success = return-ok
|
result_success = return-ok
|
||||||
}
|
}
|
||||||
|
|
||||||
userdb {
|
|
||||||
# UID number lookup (10001@example.com)
|
|
||||||
driver = ldap
|
|
||||||
args = /etc/dovecot/freedombox-ldap-userdb-uid.conf.ext
|
|
||||||
result_failure = continue
|
|
||||||
result_internalfail = return-fail
|
|
||||||
result_success = return-ok
|
|
||||||
}
|
|
||||||
|
|
||||||
userdb {
|
userdb {
|
||||||
driver = ldap
|
driver = ldap
|
||||||
args = /etc/dovecot/freedombox-ldap-userdb.conf.ext
|
args = /etc/dovecot/freedombox-ldap-userdb.conf.ext
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
# Direct edits to this file will be lost!
|
# Direct edits to this file will be lost!
|
||||||
# Manage your settings on Plinth <https://localhost/plinth/apps/email_server>
|
# Manage your settings on Plinth <https://localhost/plinth/apps/email_server>
|
||||||
|
|
||||||
# Privacy: remove the recipient's UID number from email headers
|
|
||||||
lmtp_add_received_header = no
|
|
||||||
lmtp_hdr_delivery_address = original
|
|
||||||
|
|
||||||
protocol lmtp {
|
protocol lmtp {
|
||||||
mail_plugins = $mail_plugins sieve
|
mail_plugins = $mail_plugins sieve
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
# Direct edits to this file will be lost!
|
|
||||||
# Manage your settings on Plinth https://localhost/plinth/apps/email_server
|
|
||||||
|
|
||||||
uris = ldap://127.0.0.1
|
|
||||||
base = dc=thisbox
|
|
||||||
|
|
||||||
user_attrs = \
|
|
||||||
=home=%{ldap:homeDirectory}, \
|
|
||||||
=uid=%{ldap:uidNumber}, \
|
|
||||||
=gid=%{ldap:gidNumber}, \
|
|
||||||
=user=%{ldap:uid}, \
|
|
||||||
=mail=maildir:~/Maildir:LAYOUT=index
|
|
||||||
|
|
||||||
# Support user lookup by UID number
|
|
||||||
|
|
||||||
user_filter = \
|
|
||||||
(&(objectClass=posixAccount)(!(uidNumber=0))(uidNumber=%n))
|
|
||||||
|
|
||||||
# doveadm -A
|
|
||||||
|
|
||||||
iterate_attrs = =user=%{ldap:uid}
|
|
||||||
iterate_filter = (objectClass=posixAccount)
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
# This file is managed by FreedomBox
|
|
||||||
|
|
||||||
# Map user name to UID number
|
|
||||||
|
|
||||||
bind = no
|
|
||||||
server_host = ldap://127.0.0.1
|
|
||||||
search_base = dc=thisbox
|
|
||||||
query_filter = (&(objectClass=posixAccount)(uid=%s))
|
|
||||||
result_attribute = uidNumber
|
|
||||||
result_format = %s@localhost
|
|
||||||
@ -2,7 +2,6 @@
|
|||||||
"""
|
"""
|
||||||
Views for the email app.
|
Views for the email app.
|
||||||
"""
|
"""
|
||||||
import pwd
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -147,13 +146,9 @@ class AliasView(FormView):
|
|||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def _get_uid(self):
|
|
||||||
"""Return the UID of the user that made the request."""
|
|
||||||
return pwd.getpwnam(self.request.user.username).pw_uid
|
|
||||||
|
|
||||||
def _get_current_aliases(self):
|
def _get_current_aliases(self):
|
||||||
"""Return current list of aliases."""
|
"""Return current list of aliases."""
|
||||||
return aliases_module.get(self._get_uid())
|
return aliases_module.get(self.request.user.username)
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
@ -185,17 +180,18 @@ class AliasView(FormView):
|
|||||||
"""Handle a valid alias list form operation."""
|
"""Handle a valid alias list form operation."""
|
||||||
aliases = form.cleaned_data['aliases']
|
aliases = form.cleaned_data['aliases']
|
||||||
action = form.cleaned_data['action']
|
action = form.cleaned_data['action']
|
||||||
uid = self._get_uid()
|
username = self.request.user.username
|
||||||
if action == 'delete':
|
if action == 'delete':
|
||||||
aliases_module.delete(uid, aliases)
|
aliases_module.delete(username, aliases)
|
||||||
elif action == 'disable':
|
elif action == 'disable':
|
||||||
aliases_module.disable(uid, aliases)
|
aliases_module.disable(username, aliases)
|
||||||
elif action == 'enable':
|
elif action == 'enable':
|
||||||
aliases_module.enable(uid, aliases)
|
aliases_module.enable(username, aliases)
|
||||||
|
|
||||||
def _create_form_valid(self, form):
|
def _create_form_valid(self, form):
|
||||||
"""Handle a valid create alias form operation."""
|
"""Handle a valid create alias form operation."""
|
||||||
aliases_module.put(self._get_uid(), form.cleaned_data['alias'])
|
username = self.request.user.username
|
||||||
|
aliases_module.put(username, form.cleaned_data['alias'])
|
||||||
|
|
||||||
|
|
||||||
class DomainsView(FormView):
|
class DomainsView(FormView):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user