Add test case with real schleuder

This commit is contained in:
s3lph 2022-04-16 19:06:53 +02:00
parent 5f80c48aee
commit c236b6825a
14 changed files with 417 additions and 59 deletions

2
.gitignore vendored
View file

@ -8,4 +8,4 @@
**/.mypy_cache/ **/.mypy_cache/
ca.pem ca.pem
multischleuder.yml ./multischleuder.yml

View file

@ -3,6 +3,7 @@ image: python:3.9-bullseye
stages: stages:
- test - test
- coverage
- build - build
- deploy - deploy
@ -19,8 +20,8 @@ test:
script: script:
- pip3 install -e . - pip3 install -e .
- python3 -m coverage run --rcfile=setup.cfg -m unittest discover multischleuder - python3 -m coverage run --rcfile=setup.cfg -m unittest discover multischleuder
- python3 -m coverage combine artifacts:
- python3 -m coverage report --rcfile=setup.cfg - ".coverage*"
codestyle: codestyle:
stage: test stage: test
@ -35,6 +36,47 @@ mypy:
- mypy --install-types --non-interactive multischleuder - mypy --install-types --non-interactive multischleuder
- mypy multischleuder - mypy multischleuder
schleuder:
stage: test
script:
- debconf-set-selections <<<"postfix postfix/mailname string example.org"
- debconf-set-selections <<<"postfix postfix/main_mailer_type string 'Local only'"
- apt update; apt install --yes schleuder schleuder-cli postfix
- /usr/lib/postfix/configure-instance.sh -
- echo "virtual_alias_maps = static:root" >> /etc/postfix/main.cf
- /usr/sbin/postmulti -i - -p start
- schleuder-cli lists list || true
- export CERT_FPR=$(schleuder cert fingerprint | cut -d' ' -f4)
- echo " - '00000000000000000000000000000000'" >> /etc/schleuder/schleuder.yml
- |
cat > ~/.schleuder-cli/schleuder-cli.yml <<EOF
host: localhost
port: 4443
tls_fingerprint: ${CERT_FPR}
api_key: '00000000000000000000000000000000'
EOF
- /usr/bin/schleuder-api-daemon &
- sleep 5 # wait for daemons to start
- export API_DAEMON_PID=$!
- test/prepare-schleuder.sh
- pip3 install -e .
- python3 -c 'import os; print(os.listdir(".")); print(); print(os.listdir("test/"))'
- python3 -m coverage run --rcfile=setup.cfg -m multischleuder --config test/multischleuder.yml --verbose
- test/report.sh
- kill -9 ${API_DAEMON_PID} || true
- /usr/sbin/postmulti -i - -p stop
- sleep 5 # wait for daemons to terminate
artifacts:
- ".coverage*"
coverage:
state: coverage
script:
- python3 -m coverage combine
- python3 -m coverage report --rcfile=setup.cfg
build_wheel: build_wheel:

58
multischleuder.yml Normal file
View file

@ -0,0 +1,58 @@
---
api:
url: "https://localhost:4443"
token: 24125f2fe0ebc2fd853cf2e02f7599b3fa7f71a4c8e1519b
#cafile: /etc/schleuder/schleuder-certificate.pem
cafile: ca.pem
lists:
- target: test@schleuder.example.org
unmanaged:
- admin@example.org
banned:
- banned@example.org
sources:
- test-basel@schleuder.example.org
- test-bern@schleuder.example.org
- test-zurich@schleuder.example.org
from: test-owner@schleuder.example.org
smtp:
hostname: localhost
port: 8025
conflict:
interval: 604800 # 1 week
statefile: /var/lib/multischleuder/conflict.json
template: |
Hi {subscriber},
While compiling the subscriber list of {schleuder}, your
address {subscriber} was subscribed on multiple sub-lists with
different PGP keys. There may be something fishy or malicious going on,
or this may simply have been a mistake by you or a list admin.
You have only been subscribed to {schleuder} using the key you
have been subscribed with for the *longest* time:
{chosen}
Please review the following keys and talk to the admins of the
corresponding sub-lists to resolve this issue:
Fingerprint Sub-List
----------- --------
{affected}
For your convenience, this message has been encrypted with *all* of the
above keys. If you have any questions, or do not understand this
message, please refer to your local Schleuder admin, or reply to this
message.
Note that this automated message is unsigned, since MultiSchleuder does
not have access to Schleuder private keys.
Regards
MultiSchleuder {schleuder}

View file

@ -0,0 +1,5 @@
from multischleuder.main import main
main()

View file

@ -50,7 +50,10 @@ class SchleuderApi:
# Perform the actual request # Perform the actual request
req = urllib.request.Request(url, data=payload, method=method, headers=self._headers) req = urllib.request.Request(url, data=payload, method=method, headers=self._headers)
resp = urllib.request.urlopen(req, context=context) resp = urllib.request.urlopen(req, context=context)
return json.loads(resp.read().decode()) respdata: bytes = resp.read().decode()
if len(respdata) > 0:
return json.loads(respdata)
return None
def dry_run(self): def dry_run(self):
self._dry_run = True self._dry_run = True

View file

@ -163,7 +163,11 @@ class KeyConflictResolution:
now = int(datetime.utcnow().timestamp()) now = int(datetime.utcnow().timestamp())
with open(self._state_file, 'a+') as f: with open(self._state_file, 'a+') as f:
f.seek(0) f.seek(0)
try:
state: Dict[str, int] = json.load(f) state: Dict[str, int] = json.load(f)
except:
# TODO: This could lead to a situation where multischleuder becomes a spammer
state = {}
# Remove all state entries older than conflict_interval # Remove all state entries older than conflict_interval
state = {k: v for k, v in state.items() if now-v < self._interval} state = {k: v for k, v in state.items() if now-v < self._interval}
# Remove all messages not already sent recently # Remove all messages not already sent recently

View file

@ -2,6 +2,8 @@
from typing import Any, Dict, List from typing import Any, Dict, List
import argparse import argparse
import logging
import sys
import yaml import yaml
@ -60,12 +62,9 @@ def main():
ap.add_argument('--version', action='version', version=__version__) ap.add_argument('--version', action='version', version=__version__)
ns = ap.parse_args(sys.argv[1:]) ns = ap.parse_args(sys.argv[1:])
if ns.verbose: if ns.verbose:
logger = logging.getLogger().setLevel('DEBUG') logger = logging.getLogger()
logger.setLevel('DEBUG')
logger.debug('Verbose logging enabled') logger.debug('Verbose logging enabled')
lists = parse_config(ns) lists = parse_config(ns)
for lst in lists: for lst in lists:
lst.process(ns.dry_run) lst.process(ns.dry_run)
if __name__ == '__main__':
main()

View file

@ -69,8 +69,8 @@ _SUBSCRIBER_RESPONSE = '''
{ {
"id": 23, "id": 23,
"list_id": 42, "list_id": 42,
"email": "foo@example.org", "email": "andy.example@example.org",
"fingerprint": "2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6", "fingerprint": "ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9",
"admin": false, "admin": false,
"delivery_enabled": true, "delivery_enabled": true,
"created_at": "2022-04-15T01:11:12.123Z", "created_at": "2022-04-15T01:11:12.123Z",
@ -84,7 +84,7 @@ _SUBSCRIBER_RESPONSE_NOKEY = '''
{ {
"id": 24, "id": 24,
"list_id": 42, "list_id": 42,
"email": "foo@example.org", "email": "andy.example@example.org",
"fingerprint": "", "fingerprint": "",
"admin": false, "admin": false,
"delivery_enabled": true, "delivery_enabled": true,
@ -96,15 +96,15 @@ _SUBSCRIBER_RESPONSE_NOKEY = '''
_KEY_RESPONSE = ''' _KEY_RESPONSE = '''
{ {
"fingerprint": "2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6", "fingerprint": "ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9",
"email": "foo@example.org", "email": "andy.example@example.org",
"expiry": null, "expiry": null,
"generated_at": "2022-04-14T23:19:24.000Z", "generated_at": "2022-04-16T23:19:24.000Z",
"primary_uid": "Multischleuder Test Key (TEST - DO NOT USE) <foo@example.org>", "primary_uid": "Mutlischleuder Test User <andy.example@example.org>",
"oneline": "0x2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6 foo@example.org 2022-04-14", "oneline": "0xADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 andy.example@example.org 2022-04-16",
"trust_issues": null, "trust_issues": null,
"description": "pub ed25519 2022-04-14 [SC]\\n 2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6\\nuid Multischleuder Test Key (TEST - DO NOT USE) <foo@example.org>\\nsub cv25519 2022-04-14 [E]\\n", "description": "pub 256?/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 2022-04-16\\nuid\\t\\tMutlischleuder Test User <andy.example@example.org>\\nsub 256?/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 2022-04-16\\nsub 256?/C0E8ED7A32F53626F2FCDC65F5035A1D90E35CAE 2022-04-16\\n",
"ascii": "pub ed25519 2022-04-14 [SC]\\n 2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6\\nuid Multischleuder Test Key (TEST - DO NOT USE) <foo@example.org>\\nsub cv25519 2022-04-14 [E]\\n-----BEGIN PGP PUBLIC KEY BLOCK-----\\n\\nmDMEYlirsBYJKwYBBAHaRw8BAQdAGAHsSb3b3x+V6d7XouOXJryqW4mcjn1nDT2z\\nFgf5lEy0PU11bHRpc2NobGV1ZGVyIFRlc3QgS2V5IChURVNUIC0gRE8gTk9UIFVT\\nRSkgPGZvb0BleGFtcGxlLm9yZz6IkAQTFggAOBYhBC+7wN+X/b8eS3BO7eOe9PrE\\nIL62BQJiWKuwAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEOOe9PrEIL62\\nUZUA/RrRiqhz+eY5PBXWvNzN2FjL0aTsrbPsjQ3fzQ0lThQkAQD+tVjLfx495OXn\\n/Y2NwnZaKtM80FPBsy2C0u0rEd6uALg4BGJYq7ASCisGAQQBl1UBBQEBB0A5xq5f\\nUb1i2Ayvbt+ZFgxx+OL3KT12AkSkLcaAeRjKcQMBCAeIeAQYFggAIBYhBC+7wN+X\\n/b8eS3BO7eOe9PrEIL62BQJiWKuwAhsMAAoJEOOe9PrEIL624lEA/iyn0KNUx8AK\\nrSMLp7JawmsT+uD2pcw1uH3qZPHMja3gAP91/1vKQ8X5tEYzlE9OvVqtf9ESQBLj\\nzxMaMEdub5qiBQ==\\n=RCkT\\n-----END PGP PUBLIC KEY BLOCK-----\\n" "ascii": "pub 256?/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 2022-04-16\\nuid\\t\\tMutlischleuder Test User <andy.example@example.org>\\nsub 256?/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 2022-04-16\\nsub 256?/C0E8ED7A32F53626F2FCDC65F5035A1D90E35CAE 2022-04-16\\n\\n\\n-----BEGIN PGP PUBLIC KEY BLOCK-----\\n\\nmDMEYlsHSBYJKwYBBAHaRw8BAQdAhGNoFKTXFsAOR8xiC7WWDB4gv+TZq5tmPG7X\\n8C3h4my0SU11dGxpc2NobGV1ZGVyIFRlc3QgVXNlciAoVEVTVCBLRVkgRE8gTk9U\\nIFVTRSkgPGFuZHkuZXhhbXBsZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBK25vGef\\n9TzI72b6w5NI/at6dmP5BQJiWwdIAhsjBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA\\nAAoJEJNI/at6dmP54NoBAMGktGRD7fgmruTviHhERbhUX9OmPGUuH1tsUFVAsePk\\nAP0Xt8Uq876t87FIMMil7zuo7Oc/lYqS+JONd0NEOIzUD7hXBGJbB0gSCSsGAQQB\\n2kcPAQIDBHhrny0kv/i58MlgJmR0g3dyadbPGt66Yht0dY6Azkz8eAbMuPG+Gqhu\\n/txLXnzPI1Gb99i934CCFUPgsvMorEIDAQgHiHgEGBYIACAWIQStubxnn/U8yO9m\\n+sOTSP2renZj+QUCYlsHSAIbDAAKCRCTSP2renZj+R9nAQDOcZRSgl9l7Z1inKjO\\nEwaQmYg/O9xked0C5mJwlV2mdgD9Gvamm5n6djU2D91X8Wbp49upWe1rAv2EgeAQ\\na5AcmwE=\\n=RIBQ\\n-----END PGP PUBLIC KEY BLOCK-----\\n\\n"
} }
''' # noqa E501 ''' # noqa E501
@ -151,16 +151,16 @@ class TestSchleuderApi(unittest.TestCase):
# Test request data # Test request data
self.assertEqual('https://localhost:4443/subscriptions.json?list_id=42', self.assertEqual('https://localhost:4443/subscriptions.json?list_id=42',
mock.call_args_list[0][0][0].get_full_url()) mock.call_args_list[0][0][0].get_full_url())
self.assertEqual('https://localhost:4443/keys/2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6.json?list_id=42', self.assertEqual('https://localhost:4443/keys/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9.json?list_id=42',
mock.call_args_list[1][0][0].get_full_url()) mock.call_args_list[1][0][0].get_full_url())
self.assertEqual(1, len(subs)) self.assertEqual(1, len(subs))
self.assertEqual(23, subs[0].id) self.assertEqual(23, subs[0].id)
self.assertEqual('foo@example.org', subs[0].email) self.assertEqual('andy.example@example.org', subs[0].email)
self.assertEqual(42, subs[0].schleuder) self.assertEqual(42, subs[0].schleuder)
self.assertEqual(datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()), self.assertEqual(datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()),
subs[0].created_at) subs[0].created_at)
self.assertEqual('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', subs[0].key.fingerprint) self.assertEqual('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', subs[0].key.fingerprint)
self.assertEqual('foo@example.org', subs[0].key.email) self.assertEqual('andy.example@example.org', subs[0].key.email)
self.assertIn('-----BEGIN PGP PUBLIC KEY BLOCK-----', subs[0].key.blob) self.assertIn('-----BEGIN PGP PUBLIC KEY BLOCK-----', subs[0].key.blob)
self.assertEqual(42, subs[0].key.schleuder) self.assertEqual(42, subs[0].key.schleuder)
@ -173,7 +173,7 @@ class TestSchleuderApi(unittest.TestCase):
mock.call_args_list[0][0][0].get_full_url()) mock.call_args_list[0][0][0].get_full_url())
self.assertEqual(1, len(subs)) self.assertEqual(1, len(subs))
self.assertEqual(24, subs[0].id) self.assertEqual(24, subs[0].id)
self.assertEqual('foo@example.org', subs[0].email) self.assertEqual('andy.example@example.org', subs[0].email)
self.assertEqual(42, subs[0].schleuder) self.assertEqual(42, subs[0].schleuder)
self.assertEqual(datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()), self.assertEqual(datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()),
subs[0].created_at) subs[0].created_at)
@ -182,10 +182,10 @@ class TestSchleuderApi(unittest.TestCase):
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')
def test_get_subscriber(self, mock): def test_get_subscriber(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
sub = api.get_subscriber('foo@example.org', SchleuderList(42, '', '')) sub = api.get_subscriber('andy.example@example.org', SchleuderList(42, '', ''))
self.assertEqual(23, sub.id) self.assertEqual(23, sub.id)
self.assertEqual('foo@example.org', sub.email) self.assertEqual('andy.example@example.org', sub.email)
self.assertEqual('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', sub.key.fingerprint) self.assertEqual('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', sub.key.fingerprint)
self.assertEqual(42, sub.key.schleuder) self.assertEqual(42, sub.key.schleuder)
self.assertEqual(42, sub.schleuder) self.assertEqual(42, sub.schleuder)
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
@ -194,17 +194,17 @@ class TestSchleuderApi(unittest.TestCase):
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')
def test_get_subscriber_nokey(self, mock): def test_get_subscriber_nokey(self, mock):
api = self._mock_api(mock, nokey=True) api = self._mock_api(mock, nokey=True)
sub = api.get_subscriber('foo@example.org', SchleuderList(42, '', '')) sub = api.get_subscriber('andy.example@example.org', SchleuderList(42, '', ''))
self.assertEqual(24, sub.id) self.assertEqual(24, sub.id)
self.assertEqual('foo@example.org', sub.email) self.assertEqual('andy.example@example.org', sub.email)
self.assertIsNone(sub.key) self.assertIsNone(sub.key)
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')
def test_subscribe(self, mock): def test_subscribe(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
now = datetime.utcnow() now = datetime.utcnow()
key = SchleuderKey('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', 'foo@example.org', 'verylongpgpkeyblock', 42) key = SchleuderKey('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', 'andy.example@example.org', 'verylongpgpkeyblock', 42)
sub = SchleuderSubscriber(23, 'foo@example.org', key, 42, now) sub = SchleuderSubscriber(23, 'andy.example@example.org', key, 42, now)
api.subscribe(sub, SchleuderList(42, '', '')) api.subscribe(sub, SchleuderList(42, '', ''))
self.assertEqual('https://localhost:4443/subscriptions.json?list_id=42', self.assertEqual('https://localhost:4443/subscriptions.json?list_id=42',
mock.call_args_list[-1][0][0].get_full_url()) mock.call_args_list[-1][0][0].get_full_url())
@ -215,7 +215,7 @@ class TestSchleuderApi(unittest.TestCase):
def test_subscribe_nokey(self, mock): def test_subscribe_nokey(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
now = datetime.utcnow() now = datetime.utcnow()
sub = SchleuderSubscriber(23, 'foo@example.org', None, 42, now) sub = SchleuderSubscriber(23, 'andy.example@example.org', None, 42, now)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
api.subscribe(sub, SchleuderList(42, '', '')) api.subscribe(sub, SchleuderList(42, '', ''))
@ -223,8 +223,8 @@ class TestSchleuderApi(unittest.TestCase):
def test_unsubscribe(self, mock): def test_unsubscribe(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
now = datetime.utcnow() now = datetime.utcnow()
key = SchleuderKey('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', 'foo@example.org', 'verylongpgpkeyblock', 42) key = SchleuderKey('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', 'andy.example@example.org', 'verylongpgpkeyblock', 42)
sub = SchleuderSubscriber(23, 'foo@example.org', key, 42, now) sub = SchleuderSubscriber(23, 'andy.example@example.org', key, 42, now)
api.unsubscribe(sub, SchleuderList(42, '', '')) api.unsubscribe(sub, SchleuderList(42, '', ''))
self.assertEqual('https://localhost:4443/subscriptions/23.json', self.assertEqual('https://localhost:4443/subscriptions/23.json',
mock.call_args_list[-1][0][0].get_full_url()) mock.call_args_list[-1][0][0].get_full_url())
@ -235,8 +235,8 @@ class TestSchleuderApi(unittest.TestCase):
def test_update_fingerprint(self, mock): def test_update_fingerprint(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
now = datetime.utcnow() now = datetime.utcnow()
key = SchleuderKey('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', 'foo@example.org', 'verylongpgpkeyblock', 42) key = SchleuderKey('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', 'andy.example@example.org', 'verylongpgpkeyblock', 42)
sub = SchleuderSubscriber(23, 'foo@example.org', key, 42, now) sub = SchleuderSubscriber(23, 'andy.example@example.org', key, 42, now)
api.update_fingerprint(sub, SchleuderList(42, '', '')) api.update_fingerprint(sub, SchleuderList(42, '', ''))
self.assertEqual('https://localhost:4443/subscriptions/23.json', self.assertEqual('https://localhost:4443/subscriptions/23.json',
mock.call_args_list[-1][0][0].get_full_url()) mock.call_args_list[-1][0][0].get_full_url())
@ -247,25 +247,25 @@ class TestSchleuderApi(unittest.TestCase):
def test_update_fingerprint_nokey(self, mock): def test_update_fingerprint_nokey(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
now = datetime.utcnow() now = datetime.utcnow()
sub = SchleuderSubscriber(23, 'foo@example.org', None, 42, now) sub = SchleuderSubscriber(23, 'andy.example@example.org', None, 42, now)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
api.update_fingerprint(sub, SchleuderList(42, '', '')) api.update_fingerprint(sub, SchleuderList(42, '', ''))
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')
def test_get_key(self, mock): def test_get_key(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
key = api.get_key('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', SchleuderList(42, '', '')) key = api.get_key('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', SchleuderList(42, '', ''))
self.assertEqual('https://localhost:4443/keys/2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6.json?list_id=42', self.assertEqual('https://localhost:4443/keys/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9.json?list_id=42',
mock.call_args_list[0][0][0].get_full_url()) mock.call_args_list[0][0][0].get_full_url())
self.assertEqual('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', key.fingerprint) self.assertEqual('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', key.fingerprint)
self.assertEqual('foo@example.org', key.email) self.assertEqual('andy.example@example.org', key.email)
self.assertEqual(42, key.schleuder) self.assertEqual(42, key.schleuder)
self.assertIn('-----BEGIN PGP PUBLIC KEY BLOCK-----', key.blob) self.assertIn('-----BEGIN PGP PUBLIC KEY BLOCK-----', key.blob)
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')
def test_post_key(self, mock): def test_post_key(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
key = SchleuderKey('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', 'foo@example.org', 'verylongpgpkeyblock', 42) key = SchleuderKey('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', 'andy.example@example.org', 'verylongpgpkeyblock', 42)
api.post_key(key, SchleuderList(42, '', '')) api.post_key(key, SchleuderList(42, '', ''))
self.assertEqual('https://localhost:4443/keys.json?list_id=42', self.assertEqual('https://localhost:4443/keys.json?list_id=42',
mock.call_args_list[0][0][0].get_full_url()) mock.call_args_list[0][0][0].get_full_url())
@ -275,9 +275,9 @@ class TestSchleuderApi(unittest.TestCase):
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')
def test_delete_key(self, mock): def test_delete_key(self, mock):
api = self._mock_api(mock) api = self._mock_api(mock)
key = SchleuderKey('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', 'foo@example.org', 'verylongpgpkeyblock', 42) key = SchleuderKey('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', 'andy.example@example.org', 'verylongpgpkeyblock', 42)
api.delete_key(key, SchleuderList(42, '', '')) api.delete_key(key, SchleuderList(42, '', ''))
self.assertEqual('https://localhost:4443/keys/2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6.json?list_id=42', self.assertEqual('https://localhost:4443/keys/ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9.json?list_id=42',
mock.call_args_list[0][0][0].get_full_url()) mock.call_args_list[0][0][0].get_full_url())
self.assertEqual('DELETE', mock.call_args_list[0][0][0].method) self.assertEqual('DELETE', mock.call_args_list[0][0][0].method)
# todo assert request payload # todo assert request payload
@ -287,8 +287,8 @@ class TestSchleuderApi(unittest.TestCase):
api = self._mock_api(mock) api = self._mock_api(mock)
api.dry_run() api.dry_run()
now = datetime.utcnow() now = datetime.utcnow()
key = SchleuderKey('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', 'foo@example.org', 'verylongpgpkeyblock', 42) key = SchleuderKey('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', 'andy.example@example.org', 'verylongpgpkeyblock', 42)
sub = SchleuderSubscriber(23, 'foo@example.org', key, 42, now) sub = SchleuderSubscriber(23, 'andy.example@example.org', key, 42, now)
sch = SchleuderList(42, '', '') sch = SchleuderList(42, '', '')
# create, update, delete should be no-ops; 5 requests for retrieving the keys & subscriptions in unsub & update # create, update, delete should be no-ops; 5 requests for retrieving the keys & subscriptions in unsub & update
api.subscribe(sub, sch) api.subscribe(sub, sch)
@ -300,5 +300,5 @@ class TestSchleuderApi(unittest.TestCase):
# only reads should execute # only reads should execute
api.get_lists() api.get_lists()
api.get_subscribers(sch) api.get_subscribers(sch)
api.get_key('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', sch) api.get_key('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', sch)
self.assertLess(2, len(mock.call_args_list)) self.assertLess(2, len(mock.call_args_list))

View file

@ -3,6 +3,7 @@ import datetime
import json import json
import unittest import unittest
import pgpy # type: ignore
from dateutil.tz import tzutc from dateutil.tz import tzutc
from multischleuder.types import SchleuderKey, SchleuderList, SchleuderSubscriber from multischleuder.types import SchleuderKey, SchleuderList, SchleuderSubscriber
@ -19,29 +20,33 @@ class TestSchleuderTypes(unittest.TestCase):
def test_parse_key(self): def test_parse_key(self):
k = SchleuderKey.from_api(42, **json.loads(_KEY_RESPONSE)) k = SchleuderKey.from_api(42, **json.loads(_KEY_RESPONSE))
self.assertEqual('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', k.fingerprint) self.assertEqual('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', k.fingerprint)
self.assertEqual('foo@example.org', k.email) self.assertEqual('andy.example@example.org', k.email)
self.assertEqual(42, k.schleuder) self.assertEqual(42, k.schleuder)
self.assertIn('-----BEGIN PGP PUBLIC KEY BLOCK-----', k.blob) self.assertIn('-----BEGIN PGP PUBLIC KEY BLOCK-----', k.blob)
self.assertIn('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6 (foo@example.org)', str(k)) self.assertIn('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 (andy.example@example.org)', str(k))
# Make sure the key can be used by PGPy
key, _ = pgpy.PGPKey.from_blob(k.blob)
msg = pgpy.PGPMessage.new('Hello World')
key.encrypt(msg)
def test_parse_subscriber(self): def test_parse_subscriber(self):
k = SchleuderKey.from_api(42, **json.loads(_KEY_RESPONSE)) k = SchleuderKey.from_api(42, **json.loads(_KEY_RESPONSE))
s = SchleuderSubscriber.from_api(k, **json.loads(_SUBSCRIBER_RESPONSE)[0]) s = SchleuderSubscriber.from_api(k, **json.loads(_SUBSCRIBER_RESPONSE)[0])
self.assertEqual(23, s.id) self.assertEqual(23, s.id)
self.assertEqual('foo@example.org', str(s)) self.assertEqual('andy.example@example.org', str(s))
self.assertEqual('foo@example.org', s.email) self.assertEqual('andy.example@example.org', s.email)
self.assertEqual(42, s.schleuder) self.assertEqual(42, s.schleuder)
self.assertEqual(datetime.datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()), self.assertEqual(datetime.datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()),
s.created_at) s.created_at)
self.assertEqual('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6', s.key.fingerprint) self.assertEqual('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9', s.key.fingerprint)
self.assertIn('2FBBC0DF97FDBF1E4B704EEDE39EF4FAC420BEB6 (foo@example.org)', str(k)) self.assertIn('ADB9BC679FF53CC8EF66FAC39348FDAB7A7663F9 (andy.example@example.org)', str(k))
def test_parse_subscriber_nokey(self): def test_parse_subscriber_nokey(self):
s = SchleuderSubscriber.from_api(None, **json.loads(_SUBSCRIBER_RESPONSE)[0]) s = SchleuderSubscriber.from_api(None, **json.loads(_SUBSCRIBER_RESPONSE)[0])
self.assertEqual(23, s.id) self.assertEqual(23, s.id)
self.assertEqual('foo@example.org', str(s)) self.assertEqual('andy.example@example.org', str(s))
self.assertEqual('foo@example.org', s.email) self.assertEqual('andy.example@example.org', s.email)
self.assertEqual(42, s.schleuder) self.assertEqual(42, s.schleuder)
self.assertEqual(datetime.datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()), self.assertEqual(datetime.datetime(2022, 4, 15, 1, 11, 12, 123000, tzinfo=tzutc()),
s.created_at) s.created_at)

View file

@ -1,5 +1,5 @@
from typing import Optional from typing import List, Optional
from dataclasses import dataclass, field, Field from dataclasses import dataclass, field, Field
from datetime import datetime from datetime import datetime
@ -55,7 +55,16 @@ class SchleuderKey:
email: str, email: str,
ascii: str, ascii: str,
*args, **kwargs) -> 'SchleuderKey': *args, **kwargs) -> 'SchleuderKey':
return SchleuderKey(fingerprint, email, ascii, schleuder) lines: List[str] = []
state = 0
for line in ascii.splitlines():
if '-----BEGIN PGP ' in line:
state = 1
if state == 1:
lines.append(line)
if '-----END PGP ' in line:
state = 1
return SchleuderKey(fingerprint, email, '\n'.join(lines), schleuder)
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.fingerprint} ({self.email})' return f'{self.fingerprint} ({self.email})'

View file

@ -0,0 +1,58 @@
---
api:
url: "https://localhost:4443"
token: 2d039a8cfe414e55d1ec9ce9d4d787afc27050a6f630a024ae6c7dc5ab6941e5
cafile: /etc/multischleuder/schleuder-ca.pem
lists:
- target: global@schleuder.example.org
unmanaged:
- admin@example.org
banned:
- banned@example.org
sources:
- east@schleuder.example.org
- west@schleuder.example.org
- north@schleuder.example.org
- south@schleuder.example.org
from: global-owner@schleuder.example.org
smtp:
hostname: localhost
port: 8025
conflict:
interval: 604800 # 1 week
statefile: /var/lib/multischleuder/conflict.json
template: |
Hi {subscriber},
While compiling the subscriber list of {schleuder}, your
address {subscriber} was subscribed on multiple sub-lists with
different PGP keys. There may be something fishy or malicious going on,
or this may simply have been a mistake by you or a list admin.
You have only been subscribed to {schleuder} using the key you
have been subscribed with for the *longest* time:
{chosen}
Please review the following keys and talk to the admins of the
corresponding sub-lists to resolve this issue:
Fingerprint Sub-List
----------- --------
{affected}
For your convenience, this message has been encrypted with *all* of the
above keys. If you have any questions, or do not understand this
message, please refer to your local Schleuder admin, or reply to this
message.
Note that this automated message is unsigned, since MultiSchleuder does
not have access to Schleuder private keys.
Regards
MultiSchleuder {schleuder}

70
test/multischleuder.yml Normal file
View file

@ -0,0 +1,70 @@
---
api:
url: "https://localhost:4443"
token: "00000000000000000000000000000000"
cafile: /etc/schleuder/schleuder-certificate.pem
lists:
- target: test-global@schleuder.example.org
unmanaged:
- admin@example.org
- admin2@example.org
banned:
- aspammer@example.org
sources:
- test-north@schleuder.example.org
- test-east@schleuder.example.org
- test-south@schleuder.example.org
- test-west@schleuder.example.org
from: test-global-owner@schleuder.example.org
- target: test2-global@schleuder.example.org
unmanaged:
- admin@example.org
banned:
- aspammer@example.org
- anotherspammer@example.org
sources:
- test-north@schleuder.example.org
- test-east@schleuder.example.org
from: test2-global-owner@schleuder.example.org
smtp:
hostname: localhost
port: 25
conflict:
interval: 3600 # 1 hour - you don't want this in production
statefile: conflict.json
template: |
Hi {subscriber},
While compiling the subscriber list of {schleuder}, your
address {subscriber} was subscribed on multiple sub-lists with
different PGP keys. There may be something fishy or malicious going on,
or this may simply have been a mistake by you or a list admin.
You have only been subscribed to {schleuder} using the key you
have been subscribed with for the *longest* time:
{chosen}
Please review the following keys and talk to the admins of the
corresponding sub-lists to resolve this issue:
Fingerprint Sub-List
----------- --------
{affected}
For your convenience, this message has been encrypted with *all* of the
above keys. If you have any questions, or do not understand this
message, please refer to your local Schleuder admin, or reply to this
message.
Note that this automated message is unsigned, since MultiSchleuder does
not have access to Schleuder private keys.
Regards
MultiSchleuder {schleuder}

86
test/prepare-schleuder.sh Executable file
View file

@ -0,0 +1,86 @@
#!/bin/bash
function gen_key {
echo "gen_key $@"
PUID="${1}"
shift 1
cat >/tmp/keygen <<EOF
%no-protection
%no-ask-passphrase
%transient-key
Key-Type: EDDSA
Key-Curve: ed25519
Subkey-Type: ECDH
Subkey-Curve: ed25519
Expire-Date: 0
Name-Real: Mutlischleuder Test User
Name-Comment: TEST KEY DO NOT USE
Name-Email: ${PUID}
EOF
gpg --batch --full-gen-key /tmp/keygen
for uid in $@; do
gpg --batch --quick-add-uid "${PUID}" "Mutlischleuder Test User (TEST KEY DO NOT USE) <${uid}>"
done
gpg --export --armor "${PUID}" > "/tmp/${PUID}.asc"
for uid in $@; do
gpg --export --armor "${uid}" > "/tmp/${uid}.asc"
done
}
function subscribe {
schleuder-cli subscriptions new "${1}" "${2}" "/tmp/${2}.asc"
}
gen_key admin@example.org
gen_key admin2@example.org
gen_key ada.lovelace@example.org
gen_key alex.example@example.org
gen_key aspammer@example.org
gen_key anna.example@example.org
mv /tmp/anna.example@example.org.asc /tmp/anna.example@example.org.old.asc
gen_key anotherspammer@example.org
gen_key andy.example@example.org
mv /tmp/andy.example@example.org.asc /tmp/andy.example@example.org.1.asc
gen_key aaron.example@example.org aaron.example@example.net
gen_key amy.example@example.org
install -m 0700 -d /tmp/gpg
export GNUPGHOME=/tmp/gpg
gen_key anna.example@example.org
gen_key andy.example@example.org
unset GNUPGHOME
schleuder-cli lists new test@schleuder.example.org admin@example.org /tmp/admin@example.org.asc
schleuder-cli lists new test-global@schleuder.example.org admin@example.org /tmp/admin@example.org.asc
schleuder-cli lists new test-north@schleuder.example.org admin@example.org /tmp/admin@example.org.asc
schleuder-cli lists new test-east@schleuder.example.org admin@example.org /tmp/admin@example.org.asc
schleuder-cli lists new test-south@schleuder.example.org admin@example.org /tmp/admin@example.org.asc
schleuder-cli lists new test-west@schleuder.example.org admin2@example.org /tmp/admin2@example.org.asc
schleuder-cli lists new test2-global@schleuder.example.org admin2@example.org /tmp/admin2@example.org.asc
subscribe test-global@schleuder.example.org ada.lovelace@example.org # should be unsubscribed
subscribe test-global@schleuder.example.org aaron.example@example.org # should remain as-is
subscribe test-global@schleuder.example.org aaron.example@example.net # should be unsubscribed, but key should remain
subscribe test-global@schleuder.example.org alex.example@example.org # should remain as-is
schleuder-cli subscriptions new test-global@schleuder.example.org anna.example@example.org /tmp/anna.example@example.org.old.asc
# key should be updated
subscribe test-global@schleuder.example.org aspammer@example.org # should be unsubscribed
subscribe test-north@schleuder.example.org alex.example@example.org # should remain as-is
subscribe test-north@schleuder.example.org aspammer@example.org # should be ignored
schleuder-cli subscriptions new test-north@schleuder.example.org arno.example@example.org
# should not be subscribed - no key
subscribe test-east@schleuder.example.org anna.example@example.org # key should be updated
subscribe test-east@schleuder.example.org anotherspammer@example.org # should not be subscribed
subscribe test-east@schleuder.example.org aaron.example@example.org # should remain as-is
subscribe test-south@schleuder.example.org andy.example@example.org # should be subscribed despite key conflict
subscribe test-south@schleuder.example.org amy.example@example.org # should be subscribed - conflict but same key
sleep 5 # to get different subscription dates
schleuder-cli subscriptions new test-west@schleuder.example.org andy.example@example.org /tmp/andy.example@example.org.1.asc
# should not be subscribed
subscribe test-west@schleuder.example.org amy.example@example.org # should be subscribed - conflict but same key

19
test/report.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
echo Expected:
echo
echo aaron.example@example.org
echo admin@example.org
echo alex.example@example.org
echo amy.example@example.org
echo andy.example@example.org
echo anna.example@example.org
echo anotherspammer@example.org
echo -- ---
echo Actual:
echo
schleuder-cli subscriptions list test-global@schleuder.example.org
schleuder-cli keys list test-global@schleuder.example.org
cat /var/spool/mail/root