From 80c002fe810580f65b552ef15157bd4ec70d5275 Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Thu, 12 Mar 2026 11:36:44 +0100 Subject: [PATCH 1/2] Fix account creation: set recovery email, remove broken sendAs call - Set recoveryEmail to personal_email during account creation so users can use "Forgot password" to get a reset link at their personal email - Remove broken POST /users/{key}/sendAs call (Gmail API, not Admin SDK) --- .../lambda-src/team_provisioner/handler.py | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/terraform/lambda-src/team_provisioner/handler.py b/terraform/lambda-src/team_provisioner/handler.py index 63b5952..623f32e 100644 --- a/terraform/lambda-src/team_provisioner/handler.py +++ b/terraform/lambda-src/team_provisioner/handler.py @@ -1114,6 +1114,7 @@ def handle_sync_groups_and_heros(event): import string temp_password = "".join(secrets.choice(string.ascii_letters + string.digits + "!@#$%") for _ in range(24)) + personal_email = hero.get("personal_email", "") user_body = { "primaryEmail": email, "name": { @@ -1122,29 +1123,14 @@ def handle_sync_groups_and_heros(event): }, "password": temp_password, "changePasswordAtNextLogin": True, + "recoveryEmail": personal_email, } try: create_result = _google_api("POST", "/users", access_token, user_body) if create_result and not create_result.get("already_exists"): accounts_created.append(email) - logger.info("Created Google Workspace account: %s", email) - - # Send password reset to personal email so the hero can set up their account - personal_email = hero.get("personal_email", "") - if personal_email: - try: - _google_api( - "POST", - f"/users/{user_key}/sendAs", - access_token, - ) - except Exception: - # sendAs may not be available; hero can use "forgot password" flow - logger.info( - "Could not send invite for %s — hero can use forgot password with %s", - email, personal_email, - ) + logger.info("Created Google Workspace account: %s (recovery: %s)", email, personal_email) else: accounts_existed.append(email) except Exception as e: From 828df6b74322a249f16c111e2658d279891678ec Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Thu, 12 Mar 2026 11:39:21 +0100 Subject: [PATCH 2/2] Send welcome email to personal address via Gmail API on account creation After creating a Google Workspace account, send an email from the admin account to the hero's personal email with their @java.no address and instructions to use "Forgot password" to set up their account. Requires gmail.send scope added to domain-wide delegation in GCP Admin. --- .../lambda-src/team_provisioner/handler.py | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/terraform/lambda-src/team_provisioner/handler.py b/terraform/lambda-src/team_provisioner/handler.py index 623f32e..9c49697 100644 --- a/terraform/lambda-src/team_provisioner/handler.py +++ b/terraform/lambda-src/team_provisioner/handler.py @@ -117,7 +117,8 @@ def _create_jwt(payload, private_key_pem): "https://www.googleapis.com/auth/admin.directory.group " "https://www.googleapis.com/auth/admin.directory.group.member " "https://www.googleapis.com/auth/admin.directory.user " - "https://www.googleapis.com/auth/admin.directory.user.alias" + "https://www.googleapis.com/auth/admin.directory.user.alias " + "https://www.googleapis.com/auth/gmail.send" ) @@ -195,6 +196,52 @@ def _google_api(method, path, access_token, body=None): raise +def _send_welcome_email(access_token, javabin_email, personal_email, firstname): + """Send a welcome email to the hero's personal address via Gmail API. + + Uses domain-wide delegation to send as the admin email. The email instructs + the hero to set up their @java.no account via the forgot-password flow. + """ + admin_email = _get_ssm_param(GOOGLE_ADMIN_EMAIL_PARAM) + + subject = f"Your java.no account is ready — {javabin_email}" + body_text = ( + f"Hi {firstname},\n\n" + f"Your javaBin Google Workspace account has been created:\n\n" + f" Email: {javabin_email}\n\n" + f"To set your password:\n" + f" 1. Go to https://accounts.google.com\n" + f" 2. Enter {javabin_email} as your email\n" + f" 3. Click \"Forgot password\"\n" + f" 4. A password reset link will be sent to this email ({personal_email})\n\n" + f"Once signed in, you'll have access to javaBin Google Workspace services.\n\n" + f"— javaBin platform" + ) + + # Build RFC 2822 email message + import email.mime.text + msg = email.mime.text.MIMEText(body_text) + msg["To"] = personal_email + msg["From"] = admin_email + msg["Subject"] = subject + raw_message = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii") + + # Send via Gmail API (impersonating admin) + gmail_url = f"https://gmail.googleapis.com/gmail/v1/users/me/messages/send" + gmail_body = json.dumps({"raw": raw_message}).encode("utf-8") + req = urllib.request.Request(gmail_url, data=gmail_body, method="POST") + req.add_header("Authorization", f"Bearer {access_token}") + req.add_header("Content-Type", "application/json") + + try: + with urllib.request.urlopen(req) as resp: + logger.info("Sent welcome email to %s for account %s", personal_email, javabin_email) + except urllib.error.HTTPError as e: + body_text = e.read().decode("utf-8", errors="replace") + logger.error("Gmail send failed: %d %s", e.code, body_text) + raise + + def _normalize_member(member): """Normalize a member entry to a dict with email and github fields. @@ -1131,6 +1178,13 @@ def handle_sync_groups_and_heros(event): if create_result and not create_result.get("already_exists"): accounts_created.append(email) logger.info("Created Google Workspace account: %s (recovery: %s)", email, personal_email) + + # Send welcome email to personal address via Gmail API + if personal_email: + try: + _send_welcome_email(access_token, email, personal_email, hero.get("firstname", "")) + except Exception as we: + logger.warning("Could not send welcome email to %s: %s", personal_email, we) else: accounts_existed.append(email) except Exception as e: