server/app/endpoints/jira_activate_account.py (105 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""" import uuid """Handler for jira account creation""" from ..lib import middleware, config, email import asfquart import asyncio import psycopg ONE_DAY = 86400 # A day in seconds # Jira PSQL DSN JIRA_PGSQL_DSN = psycopg.conninfo.make_conninfo(**config.jirapsql.yaml) # Mappings dict for userid<->email JIRA_EMAIL_MAPPINGS = {} # Reactivation queue. No real need for permanent storage here, all requests can be ephemeral. JIRA_REACTIVATION_QUEUE = {} # ACLI command - TODO: Add to yaml?? ACLI_CMD = "/opt/latest-cli/acli.sh" async def update_jira_email_map(): """Updates the jira userid<->email mappings from psql on a daily basis""" while True: print("Updating Jira email mappings dict") try: tmp_dict = {} async with await psycopg.AsyncConnection.connect(JIRA_PGSQL_DSN) as conn: async with conn.cursor() as cur: await cur.execute("SELECT lower_user_name, email_address from cwd_user WHERE directory_id != 10000") async for row in cur: if all(x and isinstance(x, str) for x in row): # Ensure we have actual (non-empty) strings here tmp_dict[row[0]] = row[1] # Clear and refresh mappings JIRA_EMAIL_MAPPINGS.clear() JIRA_EMAIL_MAPPINGS.update(tmp_dict) except psycopg.OperationalError as e: print(f"Operational error while querying Jira PSQL: {e}") print("Retrying later...") await asyncio.sleep(ONE_DAY) # Wait a day... async def activate_account(username: str): """Activates an account through ACLI""" email_address = JIRA_EMAIL_MAPPINGS[username] proc = await asyncio.create_subprocess_exec( ACLI_CMD, *( "jira", "-v", "--action", "updateUser", "--userId", username, "--userEmail", email_address, "--activate", ), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if proc.returncode != 0: # If any errors show up in acli, bork # Test for ACLI whining but having done the job due to privacy redactions in Jira (email addresses being blank) good_bit = '"active":true' # If the ACLI JSON output has this, it means the update worked, despite ACLI complaining. if good_bit in stdout or good_bit in stderr: return # all good, ignore! print(f"Could not reactivate Jira account '{username}': {stderr}") raise AssertionError("Jira account reactivation failed due to an internal server error.") @asfquart.APP.route( "/api/jira-account-activate", methods=[ "GET", # DEBUG "POST", # Account re-activation request from user ], ) async def process_reactivation_request(): """Initial processing of an account re-activation request: - Check that username and email match - Send confirmation link to email address - Wait for confirmation... """ formdata = await asfquart.utils.formdata() session = await asfquart.session.read() jira_username = formdata.get("username") jira_email = formdata.get("email") if jira_email.lower().endswith("@apache.org"): # This is LDAP operated, don't touch! return {"success": False, "message": "Reactivation of internal ASF accounts cannot be done through this tool."} if jira_username and jira_username in JIRA_EMAIL_MAPPINGS: if JIRA_EMAIL_MAPPINGS[jira_username].lower() == jira_email.lower(): # We have a match! # Generate and send confirmation link token = str(uuid.uuid4()) verify_url = f"https://{asfquart.app.request.host}/jira-account-reactivate.html?{token}" email.from_template( "jira_account_reactivate.txt", recipient=jira_email, variables={ "verify_url": verify_url, }, thread_start=True, thread_key=f"jira-activate-{token}", ) # Store marker in our temp dict JIRA_REACTIVATION_QUEUE[token] = jira_username return {"success": True} return {"success": False, "message": "We were unable to find the account based on the information provided. Either your Jira account username, or the email address you registered it with, is incorrect."} @asfquart.APP.route( "/api/jira-account-activate-confirm", methods=[ "GET", # DEBUG "POST", # Account re-activation request from user ], ) async def process_confirm_reactivation(): """Processes confirmation link handling (and actual reactivation of an account)""" formdata = await asfquart.utils.formdata() session = await asfquart.session.read() token = formdata.get("token") if token and token in JIRA_REACTIVATION_QUEUE: # Verify token username = JIRA_REACTIVATION_QUEUE[token] del JIRA_REACTIVATION_QUEUE[token] # Remove right away, before entering the async wait if username in JIRA_EMAIL_MAPPINGS: try: await activate_account(username) except AssertionError as e: return {"success": False, "activated": False, "error": str(e)} return {"success": True, "activated": True} else: return {"success": False, "error": "Your token could not be found in our database. Please resubmit your request."} # Schedule background updater of email mappings asfquart.APP.add_background_task(update_jira_email_map)