Skip to content

Commit 59e5b33

Browse files
authored
Fix & document change_email.py (#3331)
1 parent 3490dda commit 59e5b33

File tree

3 files changed

+66
-12
lines changed

3 files changed

+66
-12
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Script to bulk-change/-repair user's scim and brig email address
1+
Script to bulk-change/-repair user's scim and brig email address (#3321, #3331)

docs/src/understand/single-sign-on/understand/main.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,22 @@ export SCIM_TOKEN_ID=$(echo $SCIM_TOKEN_FULL | jq -r .info.id)
407407

408408
The SCIM token is now contained in the `SCIM_TOKEN` environment variable.
409409

410+
EXPERT HINT: If you are a site operator and have the ok from the team admin, but not their password, you can try something like this (WARNING! TEST THIS ON NON-PROD ACCOUNTS FIRST!):
411+
412+
```
413+
export id="$(uuid -v4)"
414+
export token_="$(dd if=/dev/random bs=32 count=1 | base64)"
415+
export created_at='2023-05-30 11:15:00+0000'
416+
export team=...
417+
export descr=...
418+
echo 'INSERT INTO spar.team_provisioning_by_token (token_, team, id, created_at, idp, descr) VALUES ('\'$token_\', $team, $id, \'$created_at\', NULL, \'$descr\'');'
419+
echo 'INSERT INTO spar.team_provisioning_by_team (token_, team, id, created_at, idp, descr) VALUES ('\'$token_\', $team, $id, \'$created_at\', NULL, \'$descr\'');'
420+
echo 'SELECT * FROM spar.team_provisioning_by_team WHERE '"team = $team AND id = $id;"
421+
echo 'SELECT * FROM spar.team_provisioning_by_token WHERE '"token_ = $token_;"
422+
echo 'DELETE FROM spar.team_provisioning_by_team WHERE '"team = $team AND id = $id;"
423+
echo 'DELETE FROM spar.team_provisioning_by_token WHERE '"token_ = $token_;"
424+
```
425+
410426
You can look it up again with:
411427

412428
```{code-block} bash

hack/bin/change_emails.py

100644100755
Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@
4242
from wire import api
4343
from wire.context import Context
4444
import csv
45+
import time # for `time.sleep`. FUTUREWORK: you can probably tweak the
46+
# sleeps we've injected a lot, or remove all of them,
47+
# without doing any harm. just test it on staging first!
4548
import argparse
4649

4750
### configure this section
4851

4952
scim_token = "..."
5053
filename = './test.csv'
51-
ctx = Context(domain="localhost", version="4", service_map={'spar': 8081, 'brig': 8082})
54+
ctx = None
5255

5356
### brig, spar api
5457

@@ -72,61 +75,96 @@ def confirm_new_email(ctx, user_id, key, code):
7275
url = ctx.mkurl("brig", f"/activate")
7376
return ctx.request('GET', url, headers=({'Z-User': user_id}), params=({'key': key, 'code': code}))
7477

78+
def assert_resp(resp, status_want):
79+
if resp.response.status_code == status_want:
80+
return
81+
else:
82+
print(resp.text);
83+
# FUTUREWORK: it's not necessary to fail here; we could also
84+
# printf this and continue with the next row of the input;
85+
# later collecting all the failing cases into a list to be
86+
# handed back to the team admin for further analysis.
87+
assert False
88+
7589
### idioms
7690

7791
def confirm_email(user_id, email):
7892
r = get_activation_code(ctx, user_id, email)
79-
assert r.response.status_code == 200
93+
assert_resp(r, 200)
8094
r2 = confirm_new_email(ctx, user_id, r.json()['key'], r.json()['code'])
81-
assert r2.response.status_code == 200
95+
assert_resp(r2, 200)
8296
return r2
8397

98+
# update from one email address to another. this is called twice,
99+
# first for "to old address" to make the db state consistent, and then
100+
# for "to new address". it's possible that this should be two
101+
# functions, but so far they have no difference to suggest that.
84102
def update(user_id, email):
85103
r = get_scim_user(ctx, user_id)
86-
assert r.response.status_code == 200
104+
assert_resp(r, 200)
87105
body = dict(r.json())
88106
if body['externalId'] != email:
89107
body['externalId'] = email
90108
r2 = put_scim_user(ctx, user_id, body)
91-
assert r2.response.status_code == 200
109+
body2 = r2.json()
110+
# FUTUREWORK: there are some legitimate ways to fail in the
111+
# following line, if the email address is already the old one.
112+
# add code here to your liking in order to handle these more
113+
# gracefully. you may want to distinguish between "change to
114+
# old address to make everything consistent" (where it's ok if
115+
# you find that it's already consistent, and you don't need to
116+
# do anything) and "change to new email address" (where you
117+
# certainly want a response that says "yes, it worked").
118+
assert_resp(r2, 200)
92119
return True
93120
else:
94121
report_state('old=new', user_id)
95122
return False
96123

97124
def report_state(msg, user_id):
98125
r = get_scim_user(ctx, user_id)
99-
assert r.response.status_code == 200
126+
assert_resp(r, 200)
100127
r2 = get_brig_user(ctx, user_id)
101-
assert r.response.status_code == 200
128+
assert_resp(r2, 200)
102129
print(f"[{msg}] uid={user_id}; scim_email={r.json()['externalId']}; brig_email={r2.json()['email']}")
130+
return r.json()['externalId'], r2.json()['email']
131+
103132

104133
### process one item
105134

106135
def update_back_and_forth(user_id, old_email, new_email):
107136
assert old_email != new_email
108137

109138
# nothing has happened yet
110-
report_state('before', user_id)
139+
email_scim, email_brig = report_state('before', user_id)
111140

112141
# change to old email address to unblock pending confirmation
142+
# if brig already has the old email, setting to old email won't yield an activation code, so we don't.
113143
if update(user_id, old_email):
114-
confirm_email(user_id, old_email)
144+
if email_brig != old_email:
145+
time.sleep(4)
146+
confirm_email(user_id, old_email)
115147
report_state('between', user_id)
116148

117149
# change back to new email address
118-
assert update(user_id, new_email)
119-
confirm_email(user_id, new_email)
150+
if update(user_id, new_email):
151+
confirm_email(user_id, new_email)
152+
else:
153+
# nothing to confirm; logging happens in update_to_new
154+
pass
120155
report_state('after', user_id)
121156

122157
### process csv file
123158

124159
def main():
160+
global ctx
125161
with open(filename, newline='') as csvfile:
126162
rows = csv.reader(csvfile, delimiter=',')
127163
for row in rows:
164+
ctx = Context(domain="localhost", version="4", service_map={'spar': 8081, 'brig': 8082})
128165
[old_email, user_id, new_email] = row
129166
update_back_and_forth(user_id, old_email, new_email)
167+
time.sleep(5)
130168

131169
if __name__ == '__main__':
132170
main()

0 commit comments

Comments
 (0)