From 17b993728251d88ad23321e7c72b5f23c903646a Mon Sep 17 00:00:00 2001 From: Julian Rother <julian@cccv.de> Date: Fri, 4 Feb 2022 01:09:03 +0100 Subject: [PATCH] Constrain mail receive addresses and fix case-folding in API Previously the getmails API endpoint did not match "receive_address" values case-insensitivly like it did pre-v2. To solve this independent of database collations, all existing mail receive addresses are converted to lower-case and new/changed receive addresses are constraint to ASCII lower-case letters, digits and symbols. --- tests/test_api.py | 25 ++++ uffd/api/views.py | 2 +- uffd/mail/models.py | 18 +++ uffd/mail/templates/mail/show.html | 2 +- uffd/mail/views.py | 4 + ...5e3ac_lower_case_mail_receive_addresses.py | 29 ++++ uffd/translations/de/LC_MESSAGES/messages.mo | Bin 31934 -> 32143 bytes uffd/translations/de/LC_MESSAGES/messages.po | 127 ++++++++++-------- 8 files changed, 146 insertions(+), 61 deletions(-) create mode 100644 uffd/migrations/versions/042879d5e3ac_lower_case_mail_receive_addresses.py diff --git a/tests/test_api.py b/tests/test_api.py index a77e302d..2cefc16e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -55,3 +55,28 @@ class TestAPIAuth(UffdTestCase): def test_no_auth(self): r = self.client.get(path=url_for('testendpoint1'), follow_redirects=True) self.assertEqual(r.status_code, 401) + +class TestAPIViews(UffdTestCase): + def setUpApp(self): + self.app.config['API_CLIENTS_2'] = { + 'test': {'client_secret': 'test', 'scopes': ['getmails']}, + } + + def test_lookup(self): + r = self.client.get(path=url_for('api.getmails', receive_address='test1@example.com'), headers=[basic_auth('test', 'test')], follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json, [{'name': 'test', 'receive_addresses': ['test1@example.com', 'test2@example.com'], 'destination_addresses': ['testuser@mail.example.com']}]) + r = self.client.get(path=url_for('api.getmails', receive_address='test2@example.com'), headers=[basic_auth('test', 'test')], follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json, [{'name': 'test', 'receive_addresses': ['test1@example.com', 'test2@example.com'], 'destination_addresses': ['testuser@mail.example.com']}]) + + def test_lookup_notfound(self): + r = self.client.get(path=url_for('api.getmails', receive_address='test3@example.com'), headers=[basic_auth('test', 'test')], follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json, []) + + def test_lookup_case_folding(self): + r = self.client.get(path=url_for('api.getmails', receive_address='Test1@example.com'), headers=[basic_auth('test', 'test')], follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json, [{'name': 'test', 'receive_addresses': ['test1@example.com', 'test2@example.com'], 'destination_addresses': ['testuser@mail.example.com']}]) + diff --git a/uffd/api/views.py b/uffd/api/views.py index cf793deb..b0869878 100644 --- a/uffd/api/views.py +++ b/uffd/api/views.py @@ -115,7 +115,7 @@ def getmails(): elif key == 'name' and len(values) == 1: mails = Mail.query.filter_by(uid=values[0]).all() elif key == 'receive_address' and len(values) == 1: - mails = Mail.query.filter(Mail.receivers.any(MailReceiveAddress.address==values[0])).all() + mails = Mail.query.filter(Mail.receivers.any(MailReceiveAddress.address==values[0].lower())).all() elif key == 'destination_address' and len(values) == 1: mails = Mail.query.filter(Mail.destinations.any(MailDestinationAddress.address==values[0])).all() else: diff --git a/uffd/mail/models.py b/uffd/mail/models.py index 7734ec97..d9ce5497 100644 --- a/uffd/mail/models.py +++ b/uffd/mail/models.py @@ -1,3 +1,5 @@ +import re + from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.ext.associationproxy import association_proxy @@ -5,6 +7,18 @@ from sqlalchemy.ext.associationproxy import association_proxy from uffd.database import db class Mail(db.Model): + # Aliases are looked up by receiver addresses with api.getmails. To emulate + # the pre-v2/LDAP behaviour, the lookup needs to be case-insensitive. To not + # rely on database-specific behaviour, we ensure that all receiver addresses + # are stored lower-case and convert incoming addresses in api.getmails to + # lower-case. Note that full emulation of LDAP behaviour would also require + # whitespace normalization. Instead we disallow spaces in receiver addresses. + + # Match ASCII code points 33 (!) to 64 (@) and 91 ([) to 126 (~), i.e. any + # number of lower-case ASCII letters, digits, symbols + RECEIVER_REGEX = '[!-@[-~]*' + RECEIVER_REGEX_COMPILED = re.compile(RECEIVER_REGEX) + __tablename__ = 'mail' id = Column(Integer(), primary_key=True, autoincrement=True) uid = Column(String(32), unique=True, nullable=False) @@ -13,6 +27,10 @@ class Mail(db.Model): _destinations = relationship('MailDestinationAddress', cascade='all, delete-orphan') destinations = association_proxy('_destinations', 'address') + @property + def invalid_receivers(self): + return [addr for addr in self.receivers if not re.fullmatch(self.RECEIVER_REGEX_COMPILED, addr)] + class MailReceiveAddress(db.Model): __tablename__ = 'mail_receive_address' id = Column(Integer(), primary_key=True, autoincrement=True) diff --git a/uffd/mail/templates/mail/show.html b/uffd/mail/templates/mail/show.html index dea92a2f..44b0db5c 100644 --- a/uffd/mail/templates/mail/show.html +++ b/uffd/mail/templates/mail/show.html @@ -13,7 +13,7 @@ <label for="mail-receivers">{{_('Receiving addresses')}}</label> <textarea rows="10" class="form-control" id="mail-receivers" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea> <small class="form-text text-muted"> - {{_('One address per line')}} + {{_('One address pattern (local+ext@domain, local@domain, local, @domain) per line. Only lower-case ASCII letters, digits and symbols.')}} </small> </div> <div class="form-group col"> diff --git a/uffd/mail/views.py b/uffd/mail/views.py index 8ee57c56..6f3f0c97 100644 --- a/uffd/mail/views.py +++ b/uffd/mail/views.py @@ -41,6 +41,10 @@ def update(uid=None): mail = Mail(uid=request.form.get('mail-uid')) mail.receivers = request.form.get('mail-receivers', '').splitlines() mail.destinations = request.form.get('mail-destinations', '').splitlines() + if mail.invalid_receivers: + for addr in mail.invalid_receivers: + flash(_('Invalid receive address: %(mail_address)s', mail_address=addr)) + return render_template('mail/show.html', mail=mail) db.session.add(mail) db.session.commit() flash(_('Mail mapping updated.')) diff --git a/uffd/migrations/versions/042879d5e3ac_lower_case_mail_receive_addresses.py b/uffd/migrations/versions/042879d5e3ac_lower_case_mail_receive_addresses.py new file mode 100644 index 00000000..cfa2b813 --- /dev/null +++ b/uffd/migrations/versions/042879d5e3ac_lower_case_mail_receive_addresses.py @@ -0,0 +1,29 @@ +"""lower-case mail receive addresses + +Revision ID: 042879d5e3ac +Revises: 878b25c4fae7 +Create Date: 2022-02-01 20:37:32.103288 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '042879d5e3ac' +down_revision = '878b25c4fae7' +branch_labels = None +depends_on = None + +def upgrade(): + meta = sa.MetaData(bind=op.get_bind()) + mail_receive_address_table = sa.Table('mail_receive_address', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('mail_id', sa.Integer(), nullable=False), + sa.Column('address', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['mail_id'], ['mail.id'], name=op.f('fk_mail_receive_address_mail_id_mail'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mail_receive_address')) + ) + op.execute(mail_receive_address_table.update().values(address=sa.func.lower(mail_receive_address_table.c.address))) + +def downgrade(): + pass diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo index 7c1c953373dc7e65bbf123a966f8c7efd0507552..7178b9b68894ad12809ce2d9fae534e339fbd7e1 100644 GIT binary patch delta 5527 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 NaLvryyd+vm5dgT;^>qLM delta 5285 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 GQi=d!6QZ5~ diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po index 85347a75..217cba4f 100644 --- a/uffd/translations/de/LC_MESSAGES/messages.po +++ b/uffd/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-10-04 00:22+0200\n" +"POT-Creation-Date: 2022-02-03 16:51+0100\n" "PO-Revision-Date: 2021-05-25 21:18+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language: de\n" @@ -62,36 +62,36 @@ msgstr "" "Einladungslink muss entweder Account-Registrierung erlauben oder Rollen " "vergeben" -#: uffd/invite/views.py:122 uffd/invite/views.py:157 +#: uffd/invite/views.py:111 uffd/invite/views.py:146 msgid "Invalid invite link" msgstr "Ungültiger Einladungslink" -#: uffd/invite/views.py:140 +#: uffd/invite/views.py:129 msgid "Roles successfully updated" msgstr "Rollen erfolgreich geändert" -#: uffd/invite/views.py:160 +#: uffd/invite/views.py:149 msgid "Invite link does not allow signup" msgstr "Einladungslink erlaubt keine Account-Registrierung" -#: uffd/invite/views.py:186 uffd/selfservice/views.py:50 -#: uffd/signup/views.py:50 +#: uffd/invite/views.py:175 uffd/selfservice/views.py:50 +#: uffd/signup/views.py:49 msgid "Passwords do not match" msgstr "Die Passwörter stimmen nicht überein" -#: uffd/invite/views.py:191 uffd/signup/views.py:55 +#: uffd/invite/views.py:180 uffd/signup/views.py:54 #, python-format msgid "Too many signup requests with this mail address! Please wait %(delay)s." msgstr "" "Zu viele Account-Registrierungen mit dieser E-Mail-Adresse! Bitte warte " "%(delay)s." -#: uffd/invite/views.py:193 uffd/signup/views.py:57 uffd/signup/views.py:106 +#: uffd/invite/views.py:182 uffd/signup/views.py:56 uffd/signup/views.py:92 #, python-format msgid "Too many requests! Please wait %(delay)s." msgstr "Zu viele Anfragen! Bitte warte %(delay)s." -#: uffd/invite/views.py:206 uffd/signup/views.py:69 +#: uffd/invite/views.py:195 uffd/signup/views.py:68 msgid "Cound not send mail" msgstr "Mailversand fehlgeschlagen" @@ -267,8 +267,8 @@ msgstr "Enthaltene Rollen" #: uffd/mfa/templates/mfa/setup.html:158 uffd/mfa/templates/mfa/setup.html:169 #: uffd/role/templates/role/list.html:14 #: uffd/rolemod/templates/rolemod/list.html:9 -#: uffd/rolemod/templates/rolemod/show.html:46 -#: uffd/selfservice/templates/selfservice/self.html:101 +#: uffd/rolemod/templates/rolemod/show.html:44 +#: uffd/selfservice/templates/selfservice/self.html:97 #: uffd/user/templates/group/list.html:15 #: uffd/user/templates/group/show.html:26 #: uffd/user/templates/user/show.html:106 @@ -279,10 +279,10 @@ msgstr "Name" #: uffd/invite/templates/invite/new.html:36 #: uffd/role/templates/role/list.html:15 uffd/role/templates/role/show.html:48 #: uffd/rolemod/templates/rolemod/list.html:10 -#: uffd/rolemod/templates/rolemod/show.html:28 -#: uffd/selfservice/templates/selfservice/self.html:102 +#: uffd/rolemod/templates/rolemod/show.html:26 +#: uffd/selfservice/templates/selfservice/self.html:98 #: uffd/user/templates/group/list.html:16 -#: uffd/user/templates/group/show.html:30 +#: uffd/user/templates/group/show.html:33 #: uffd/user/templates/user/show.html:107 #: uffd/user/templates/user/show.html:139 msgid "Description" @@ -295,7 +295,7 @@ msgstr "Link erstellen" #: uffd/invite/templates/invite/new.html:56 #: uffd/mail/templates/mail/show.html:28 uffd/mfa/templates/mfa/auth.html:33 #: uffd/role/templates/role/show.html:14 -#: uffd/rolemod/templates/rolemod/show.html:11 +#: uffd/rolemod/templates/rolemod/show.html:9 #: uffd/session/templates/session/deviceauth.html:39 #: uffd/session/templates/session/deviceauth.html:49 #: uffd/session/templates/session/devicelogin.html:29 @@ -355,10 +355,15 @@ msgid "Forwardings" msgstr "Weiterleitungen" #: uffd/mail/views.py:46 +#, python-format +msgid "Invalid receive address: %(mail_address)s" +msgstr "Ungültige Empfangsadresse: %(mail_address)s" + +#: uffd/mail/views.py:50 msgid "Mail mapping updated." msgstr "Mailweiterleitung geändert." -#: uffd/mail/views.py:55 +#: uffd/mail/views.py:59 msgid "Deleted mail mapping." msgstr "Mailweiterleitung gelöscht." @@ -370,12 +375,20 @@ msgstr "Empfangsadressen" msgid "Destinations" msgstr "Zieladressen" -#: uffd/mail/templates/mail/show.html:16 uffd/mail/templates/mail/show.html:23 +#: uffd/mail/templates/mail/show.html:16 +msgid "" +"One address pattern (local+ext@domain, local@domain, local, @domain) per " +"line. Only lower-case ASCII letters, digits and symbols." +msgstr "" +"Ein Adressmuster (local+ext@domain, local@domain, local, @domain) pro " +"Zeile. Nur ASCII-Kleinbuchstaben, -Ziffern und -Symbole." + +#: uffd/mail/templates/mail/show.html:23 msgid "One address per line" msgstr "Eine Adresse pro Zeile" #: uffd/mail/templates/mail/show.html:27 uffd/role/templates/role/show.html:13 -#: uffd/rolemod/templates/rolemod/show.html:10 +#: uffd/rolemod/templates/rolemod/show.html:8 #: uffd/user/templates/group/show.html:8 uffd/user/templates/user/show.html:7 msgid "Save" msgstr "Speichern" @@ -816,7 +829,7 @@ msgid "Set as default" msgstr "Als Default setzen" #: uffd/role/templates/role/show.html:19 uffd/role/templates/role/show.html:21 -#: uffd/selfservice/templates/selfservice/self.html:117 +#: uffd/selfservice/templates/selfservice/self.html:112 #: uffd/user/templates/group/show.html:11 uffd/user/templates/user/show.html:10 #: uffd/user/templates/user/show.html:94 msgid "Are you sure?" @@ -855,8 +868,8 @@ msgid "Moderators" msgstr "Accounts mit Moderationsrechten" #: uffd/role/templates/role/show.html:71 -#: uffd/rolemod/templates/rolemod/show.html:18 -#: uffd/user/templates/group/show.html:37 +#: uffd/rolemod/templates/rolemod/show.html:16 +#: uffd/user/templates/group/show.html:40 msgid "Members" msgstr "Mitglieder" @@ -892,27 +905,27 @@ msgstr "Beschreibung zu lang" msgid "Member removed" msgstr "Mitglied entfernt" -#: uffd/rolemod/templates/rolemod/show.html:8 +#: uffd/rolemod/templates/rolemod/show.html:7 msgid "Invite Members" msgstr "Mitglieder einladen" -#: uffd/rolemod/templates/rolemod/show.html:15 +#: uffd/rolemod/templates/rolemod/show.html:13 msgid "Overview" msgstr "Übersicht" -#: uffd/rolemod/templates/rolemod/show.html:24 +#: uffd/rolemod/templates/rolemod/show.html:22 msgid "Role name" msgstr "Rollenname" -#: uffd/rolemod/templates/rolemod/show.html:32 +#: uffd/rolemod/templates/rolemod/show.html:30 msgid "Moderators:" msgstr "Accounts mit Moderationsrechten:" -#: uffd/rolemod/templates/rolemod/show.html:42 +#: uffd/rolemod/templates/rolemod/show.html:40 msgid "Role members:" msgstr "Mitglieder:" -#: uffd/rolemod/templates/rolemod/show.html:55 +#: uffd/rolemod/templates/rolemod/show.html:53 msgid "Remove" msgstr "Entfernen" @@ -957,28 +970,27 @@ msgstr "" "Falls E-Mail-Adresse und Anmeldename richtig waren, wurde eine E-Mail an " "die Adresse gesendet." -#: uffd/selfservice/views.py:91 uffd/selfservice/views.py:100 -#: uffd/selfservice/views.py:132 uffd/selfservice/views.py:142 +#: uffd/selfservice/views.py:87 uffd/selfservice/views.py:116 msgid "Token expired, please try again." msgstr "Link abgelaufen, bitte versuche es erneut." -#: uffd/selfservice/views.py:108 +#: uffd/selfservice/views.py:95 msgid "You need to set a password, please try again." msgstr "Password fehlt, bitte versuche es erneut." -#: uffd/selfservice/views.py:111 +#: uffd/selfservice/views.py:98 msgid "Passwords do not match, please try again." msgstr "Die Passwörter stimmen nicht überein, bitte versuche es erneut" -#: uffd/selfservice/views.py:116 +#: uffd/selfservice/views.py:103 msgid "Password ist not valid, please try again." msgstr "Ungültiges Passwort, bitte versuche es erneut" -#: uffd/selfservice/views.py:119 +#: uffd/selfservice/views.py:106 msgid "New password set" msgstr "Passwort geändert" -#: uffd/selfservice/views.py:148 +#: uffd/selfservice/views.py:122 msgid "" "This link was generated for another user. Login as the correct user to " "continue." @@ -986,20 +998,16 @@ msgstr "" "Dieser Link wurde für einen anderen Account erstellt. Melde dich mit dem " "richtigen Account an um Fortzufahren." -#: uffd/selfservice/views.py:150 +#: uffd/selfservice/views.py:124 msgid "New mail set" msgstr "E-Mail-Adresse geändert" -#: uffd/selfservice/views.py:160 -msgid "Leaving roles is disabled" -msgstr "Verlassen von Rollen ist deaktiviert" - -#: uffd/selfservice/views.py:166 +#: uffd/selfservice/views.py:137 #, python-format msgid "You left role %(role_name)s" msgstr "Rolle %(role_name)s verlassen" -#: uffd/selfservice/views.py:177 uffd/selfservice/views.py:194 +#: uffd/selfservice/views.py:148 uffd/selfservice/views.py:165 #, python-format msgid "Mail to \"%(mail_address)s\" could not be sent!" msgstr "E-Mail an \"%(mail_address)s\" konnte nicht gesendet werden!" @@ -1137,17 +1145,13 @@ msgstr "" "Auf <a href=\"%(services_url)s\">Dienste</a> erhälst du einen Überblick " "über deine aktuellen Berechtigungen." -#: uffd/selfservice/templates/selfservice/self.html:94 +#: uffd/selfservice/templates/selfservice/self.html:93 msgid "Administrators and role moderators can invite you to new roles." msgstr "" "Accounts mit Adminrechten oder Rollen-Moderationsrechten können dich zu " "Rollen einladen." -#: uffd/selfservice/templates/selfservice/self.html:96 -msgid "Administrators can add new roles to your account." -msgstr "Accounts mit Adminrechten können dich zu neuen Rollen hinzufügen." - -#: uffd/selfservice/templates/selfservice/self.html:111 +#: uffd/selfservice/templates/selfservice/self.html:107 msgid "" "Some permissions in this role require you to setup two-factor " "authentication" @@ -1155,11 +1159,11 @@ msgstr "" "Einige Berechtigungen dieser Rolle erfordern das Einrichten von Zwei-" "Faktor-Authentifikation" -#: uffd/selfservice/templates/selfservice/self.html:118 +#: uffd/selfservice/templates/selfservice/self.html:113 msgid "Leave" msgstr "Verlassen" -#: uffd/selfservice/templates/selfservice/self.html:126 +#: uffd/selfservice/templates/selfservice/self.html:120 msgid "You currently don't have any roles" msgstr "Du hast derzeit keine Rollen" @@ -1323,28 +1327,28 @@ msgstr "Über anderes Gerät anmelden" msgid "Register" msgstr "Registrieren" -#: uffd/session/templates/session/login.html:30 +#: uffd/session/templates/session/login.html:29 msgid "Forgot Password?" msgstr "Passwort vergessen?" -#: uffd/signup/views.py:24 +#: uffd/signup/views.py:23 msgid "Singup not enabled" msgstr "Account-Registrierung ist deaktiviert" -#: uffd/signup/views.py:82 uffd/signup/views.py:91 uffd/signup/views.py:99 +#: uffd/signup/views.py:77 uffd/signup/views.py:85 msgid "Invalid signup link" msgstr "Ungültiger Account-Registrierungs-Link" -#: uffd/signup/views.py:104 +#: uffd/signup/views.py:90 #, python-format msgid "Too many failed attempts! Please wait %(delay)s." msgstr "Zu viele fehlgeschlagene Versuche! Bitte warte mindestens %(delay)s." -#: uffd/signup/views.py:110 +#: uffd/signup/views.py:96 msgid "Wrong password" msgstr "Falsches Passwort" -#: uffd/signup/views.py:116 +#: uffd/signup/views.py:102 msgid "Your account was successfully created" msgstr "Account erfolgreich erstellt" @@ -1365,6 +1369,7 @@ msgid "Check" msgstr "Überprüfen" #: uffd/signup/templates/signup/start.html:23 +#: uffd/user/templates/group/show.html:29 msgid "" "At least one and at most 32 lower-case characters, digits, dashes (\"-\")" " or underscores (\"_\"). <b>Cannot be changed later!</b>" @@ -1455,19 +1460,23 @@ msgstr "" msgid "Groups" msgstr "Gruppen" -#: uffd/user/views_group.py:51 +#: uffd/user/views_group.py:42 +msgid "Invalid name" +msgstr "Ungültiger Name" + +#: uffd/user/views_group.py:53 msgid "Group with this name or id already exists" msgstr "Gruppe mit diesem Namen oder dieser ID existiert bereits" -#: uffd/user/views_group.py:56 +#: uffd/user/views_group.py:58 msgid "Group created" msgstr "Gruppe erstellt" -#: uffd/user/views_group.py:58 +#: uffd/user/views_group.py:60 msgid "Group updated" msgstr "Gruppe aktualisiert" -#: uffd/user/views_group.py:67 +#: uffd/user/views_group.py:69 msgid "Deleted group" msgstr "Gruppe gelöscht" -- GitLab