server/app/endpoints/mailinglist.py (103 lines of code) (raw):

#!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Selfserve Portal for the Apache Software Foundation""" """Handler for mailing list creation""" if not __debug__: raise RuntimeError("This code requires assert statements to be enabled") from ..lib import middleware, config, asfuid, email, log, utils import asfquart import asfquart.auth import asfquart.session from asfquart.auth import Requirements as R import time import json import os import re VALID_LISTPART_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") # These lists are accepted as private (and MUST be private). All other lists should be public unless root. PRIVATE_LISTS = ( "private", "security", ) # Valid moderator options that ezmlm-make accepts VALID_MUOPTS = ("mu", "Mu", "mU") VALID_MUOPTS_INFRA = ("mu", "mU", "Mu", "MU") # Infra can use MU as well # List parts cannot end in -default or -owner INVALID_ENDINGS = ( "-default", "-owner", ) def can_manage_domain(session, domain: str): """Yields true if the user can manage a specific project domain, otherwise False""" if session.isRoot is True: # Root can always manage return True for project in session.committees: if config.messaging.mail_mappings.get(project) == domain: return True return False @asfquart.APP.route( "/api/mailinglist", methods=[ "POST", # Create a new mailing list ], ) @asfquart.auth.require({R.pmc_member}) async def process_lists(): form_data = await asfquart.utils.formdata() session = await asfquart.session.read() # Creating a new mailing list listpart = form_data.get("listpart") domainpart = form_data.get("domainpart") moderators = form_data.get("moderators") is_private = form_data.get("private", False) muopts = form_data.get("muopts") trailer = form_data.get("trailer", False) expedited = form_data.get("expedited") now = int(time.time()) # Validate data try: assert listpart and VALID_LISTPART_RE.match( listpart ), "Invalid list name. Must only consist of alphanumerical characters and dashes" assert listpart.endswith("-digest") is False, "A mailing list cannot end in -digest" assert domainpart in config.messaging.mail_mappings.values(), "Mailing list domain is not a valid ASF hostname" assert can_manage_domain(session, domainpart), "You are not authorized to create mailing lists for this domain" assert isinstance(moderators, list) and moderators, "You need to provide a list of moderators" assert all( utils.check_email_address(moderator) for moderator in moderators ), "Invalid moderator list provided. Please use valid email addresses only" assert not is_private or ( listpart in PRIVATE_LISTS or session.isRoot is True ), "Only private@ or security@ can be made private by default. Please file a ticket with Infrastructure for non-standard private lists" assert is_private or listpart not in PRIVATE_LISTS, "private@ and security@ lists MUST be marked as private" assert muopts in VALID_MUOPTS or (session.isRoot is True and muopts in VALID_MUOPTS_INFRA), "Invalid moderation options given" assert isinstance(trailer, bool), "Trailer option must be a boolean value" assert not expedited or session.isRoot, "Only infrastructure can expedite mailing list requests" assert f"{listpart}@{domainpart}" not in config.messaging.mailing_lists, "This mailing already exists" assert not any(listpart.endswith(bad_ending) for bad_ending in INVALID_ENDINGS), "Invalid list name. Cannot end in a restricted ezmlm keyword" except AssertionError as e: return {"success": False, "message": str(e)} # This filename is also the ID of the request. filename = f"mailinglist-{listpart}-{domainpart}.json" # Generate the payload for mailreq request_time = now if expedited: # If expedited request, for backwards compat, we pretend it came in a day earlier. request_time -= 86400 payload = { "type": "mailinglist", "id": filename, "requester": session.uid, "requested": request_time, "domain": domainpart, "list": listpart, "muopts": muopts, "private": is_private, "mods": moderators, "trailer": "t" if trailer else "T", "expedited": expedited, } # Save payload file filepath = os.path.join(config.storage.queue_dir, filename) with open(filepath, "w") as f: json.dump(payload, f) # Notify of pending request visitype = "private" if is_private else "public" await log.slack( f"A new {visitype} mailing list, `{listpart}@{domainpart}` has been queued for creation, as requested by {session.uid}@apache.org." ) email.from_template( "mailinglist_create.txt", recipient=("private@infra.apache.org", f"{session.uid}@apache.org"), variables={ "listpart": listpart, "domainpart": domainpart, "requester": session.uid, }, ) # All done for now return { "success": True, "message": "Request logged. Please allow for up to 24 hours for the request to be processed.", }