From f7df64cd183b08fd0dfd0ca0087a9d2394dce309 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 26 May 2023 23:23:21 +0200 Subject: [PATCH] Script to bulk-change/-repair user's scim and brig email address (#3321) Co-authored-by: Matthias Fischmann --- .../5-internal/pr-3321-scim-email-script | 1 + hack/bin/change_emails.py | 132 ++++++++++++++++++ hack/python/wire/api.py | 4 +- 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5-internal/pr-3321-scim-email-script create mode 100644 hack/bin/change_emails.py diff --git a/changelog.d/5-internal/pr-3321-scim-email-script b/changelog.d/5-internal/pr-3321-scim-email-script new file mode 100644 index 00000000000..3d9b48c50f6 --- /dev/null +++ b/changelog.d/5-internal/pr-3321-scim-email-script @@ -0,0 +1 @@ + Script to bulk-change/-repair user's scim and brig email address \ No newline at end of file diff --git a/hack/bin/change_emails.py b/hack/bin/change_emails.py new file mode 100644 index 00000000000..374424c0d60 --- /dev/null +++ b/hack/bin/change_emails.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +# Repair pending email confirmations after scim email update +# +# When updating email addresses (ie., externalIds) via scim in a team +# where users should have their email addresses validated, when a user +# doesn't follow up on the confirmation email (aka validation email), +# after expiration of the validation credentials, the system goes into +# an undesired state: +# +# ``` +# brig.user.email = +# brig.user.unvalidated_email = null +# spar.scim_external.external_id = +# ``` +# +# There is no way for the team/scim admins to recover from this state, +# since a scim update is ignored and doesn't send a new confirmation +# email. +# +# If you have this problem, AND YOU HAVE CONFIRMED INTERFERING DOING +# THIS IS LEGAL AND EITHER HAS OR DOES NOT LEGALLY REQUIRE THE CONSENT +# OF THE AFFECTED USERS, then you can run this script instead. +# +# Start by editing Section `configure this section` to your taste. You +# may need to create a new scim token for this, and +# https://docs.wire.com/understand/single-sign-on/understand/main.html#using-scim-via-curl +# may prove useful in that if you are customer support and have no +# account in the team. Remember to clean up after yourself and remove +# the token when you're done! +# +# `filename` should consist of rows of the form +# `old_email,user_id,new_email`. +# +# The script goes through this file, and for every user: (1) changes the +# email address to `old_email` and emulates the confirmation flow (the +# user doesn't have to do anything); (2) then changes the email address +# to `new_email`, again emulating the confirmation flow. (1) is needed +# because changing to the new email twice will cause spar to notice that +# the external_id is already `new_email`, and doesn't do anything. + +from wire import api +from wire.context import Context +import csv +import argparse + +### configure this section + +scim_token = "..." +filename = './test.csv' +ctx = Context(domain="localhost", version="4", service_map={'spar': 8081, 'brig': 8082}) + +### brig, spar api + +def get_scim_user(ctx, user_id): + url = ctx.mkurl("spar", f"scim/v2/Users/{user_id}") + return ctx.request('GET', url, headers=({'Authorization': f'Bearer {scim_token}'})) + +def get_brig_user(ctx, user_id): + url = ctx.mkurl("brig", "self") + return ctx.request('GET', url, headers=({'Z-User': user_id})) + +def put_scim_user(ctx, user_id, body): + url = ctx.mkurl("spar", f"scim/v2/Users/{user_id}") + return ctx.request('PUT', url, headers=({'Authorization': f'Bearer {scim_token}'}), json=body) + +def get_activation_code(ctx, user_id, email): + url = ctx.mkurl("brig", f"i/users/activation-code", internal=True) + return ctx.request('GET', url, params=({'email': email})) + +def confirm_new_email(ctx, user_id, key, code): + url = ctx.mkurl("brig", f"/activate") + return ctx.request('GET', url, headers=({'Z-User': user_id}), params=({'key': key, 'code': code})) + +### idioms + +def confirm_email(user_id, email): + r = get_activation_code(ctx, user_id, email) + assert r.response.status_code == 200 + r2 = confirm_new_email(ctx, user_id, r.json()['key'], r.json()['code']) + assert r2.response.status_code == 200 + return r2 + +def update(user_id, email): + r = get_scim_user(ctx, user_id) + assert r.response.status_code == 200 + body = dict(r.json()) + if body['externalId'] != email: + body['externalId'] = email + r2 = put_scim_user(ctx, user_id, body) + assert r2.response.status_code == 200 + return True + else: + report_state('old=new', user_id) + return False + +def report_state(msg, user_id): + r = get_scim_user(ctx, user_id) + assert r.response.status_code == 200 + r2 = get_brig_user(ctx, user_id) + assert r.response.status_code == 200 + print(f"[{msg}] uid={user_id}; scim_email={r.json()['externalId']}; brig_email={r2.json()['email']}") + +### process one item + +def update_back_and_forth(user_id, old_email, new_email): + assert old_email != new_email + + # nothing has happened yet + report_state('before', user_id) + + # change to old email address to unblock pending confirmation + if update(user_id, old_email): + confirm_email(user_id, old_email) + report_state('between', user_id) + + # change back to new email address + assert update(user_id, new_email) + confirm_email(user_id, new_email) + report_state('after', user_id) + +### process csv file + +def main(): + with open(filename, newline='') as csvfile: + rows = csv.reader(csvfile, delimiter=',') + for row in rows: + [old_email, user_id, new_email] = row + update_back_and_forth(user_id, old_email, new_email) + +if __name__ == '__main__': + main() diff --git a/hack/python/wire/api.py b/hack/python/wire/api.py index b7baafd4d20..5e62c92e397 100644 --- a/hack/python/wire/api.py +++ b/hack/python/wire/api.py @@ -17,8 +17,8 @@ def random_letters(n=10): return "".join(random.choices(string.ascii_letters, k=n)) -def random_email(): - return "test-email" + "-" + random_letters(10) + "@example.com" +def random_email(domain='example.com'): + return "test-email" + "-" + random_letters(10) + "@" + domain def create_user(ctx, email=None, password=None, name=None, create_team=False, **kwargs):