2 # Copyright (C) The Arvados Authors. All rights reserved.
4 # SPDX-License-Identifier: Apache-2.0
18 parser = argparse.ArgumentParser(description='Migrate users to federated identity, see https://doc.arvados.org/admin/merge-remote-account.html')
19 parser.add_argument('--tokens', type=str, required=False)
20 group = parser.add_mutually_exclusive_group(required=True)
21 group.add_argument('--report', type=str, help="Generate report .csv file listing users by email address and their associated Arvados accounts")
22 group.add_argument('--migrate', type=str, help="Consume report .csv and migrate users to designated Arvados accounts")
23 group.add_argument('--dry-run', type=str, help="Consume report .csv and report how user would be migrated to designated Arvados accounts")
24 group.add_argument('--check', action="store_true", help="Check that tokens are usable and the federation is well connected")
25 args = parser.parse_args()
31 print("Reading %s" % args.tokens)
32 with open(args.tokens, "rt") as f:
33 for r in csv.reader(f):
36 print("Contacting %s" % (host))
37 arv = arvados.api(host=host, token=token, cache=False)
38 clusters[arv._rootDesc["uuidPrefix"]] = arv
40 arv = arvados.api(cache=False)
41 rh = arv._rootDesc["remoteHosts"]
42 tok = arv.api_client_authorizations().current().execute()
43 token = "v2/%s/%s" % (tok["uuid"], tok["api_token"])
45 for k,v in rh.items():
46 arv = arvados.api(host=v, token=token, cache=False, insecure=os.environ.get("ARVADOS_API_HOST_INSECURE"))
47 config = arv.configs().get().execute()
48 if config["Login"]["LoginCluster"] != "" and loginCluster is None:
49 loginCluster = config["Login"]["LoginCluster"]
52 print("Checking that the federation is well connected")
53 for arv in clusters.values():
54 config = arv.configs().get().execute()
55 if loginCluster and config["Login"]["LoginCluster"] != loginCluster and config["ClusterID"] != loginCluster:
56 errors.append("Inconsistent login cluster configuration, expected '%s' on %s but was '%s'" % (loginCluster, config["ClusterID"], config["Login"]["LoginCluster"]))
59 cur = arv.users().current().execute()
60 #arv.api_client_authorizations().list(limit=1).execute()
61 except arvados.errors.ApiError as e:
62 errors.append("checking token for %s %s" % (arv._rootDesc["rootUrl"], e))
63 errors.append(' This script requires a token issued to a trusted client in order to manipulate access tokens.')
64 errors.append(' See "Trusted client setting" in https://doc.arvados.org/install/install-workbench-app.html')
65 errors.append(' and https://doc.arvados.org/api/tokens.html')
68 if not cur["is_admin"]:
69 errors.append("Not admin of %s" % host)
73 if r != arv._rootDesc["uuidPrefix"] and r not in arv._rootDesc["remoteHosts"]:
74 errors.append("%s is missing from remoteHosts of %s" % (r, arv._rootDesc["uuidPrefix"]))
75 for r in arv._rootDesc["remoteHosts"]:
76 if r != "*" and r not in clusters:
77 print("WARNING: %s is federated with %s but %s is missing from the tokens file or the token is invalid" % (arv._rootDesc["uuidPrefix"], r, r))
81 print("ERROR: "+str(e))
85 print("Tokens file passed checks")
92 for c, arv in clusters.items():
93 print("Getting user list from %s" % c)
94 ul = arvados.util.list_all(arv.users().list)
96 if l["uuid"].startswith(c):
99 users = sorted(users, key=lambda u: u["email"]+"::"+(u["username"] or "")+"::"+u["uuid"])
104 if u["uuid"].endswith("-anonymouspublic") or u["uuid"].endswith("-000000000000000"):
106 if lastemail == None:
107 lastemail = u["email"]
108 if u["email"] == lastemail:
115 if a["uuid"] != homeuuid:
118 r = (a["email"], a["username"], a["uuid"], loginCluster or homeuuid[0:5])
119 by_email.setdefault(a["email"], {})
120 by_email[a["email"]][a["uuid"]] = r
122 lastemail = u["email"]
129 if a["uuid"] != homeuuid:
132 r = (a["email"], a["username"], a["uuid"], loginCluster or homeuuid[0:5])
133 by_email.setdefault(a["email"], {})
134 by_email[a["email"]][a["uuid"]] = r
138 out = csv.writer(open(args.report, "wt"))
139 out.writerow(("email", "username", "user uuid", "home cluster"))
142 print("Wrote %s" % args.report)
145 if args.migrate or args.dry_run:
147 print("Performing dry run")
151 with open(args.migrate or args.dry_run, "rt") as f:
152 for r in csv.reader(f):
155 by_email.setdefault(r[0], {})
156 by_email[r[0]][r[2]] = r
166 print("(%s) Skipping %s, no home cluster specified" % (email, old_user_uuid))
167 if old_user_uuid.startswith(userhome):
168 migratecluster = old_user_uuid[0:5]
169 migratearv = clusters[migratecluster]
170 if migratearv.users().get(uuid=old_user_uuid).execute()["username"] != username:
171 print("(%s) Updating username of %s to '%s' on %s" % (email, old_user_uuid, username, migratecluster))
174 conflicts = migratearv.users().list(filters=[["username", "=", username]]).execute()
175 if conflicts["items"]:
176 migratearv.users().update(uuid=conflicts["items"][0]["uuid"], body={"user": {"username": username+"migrate"}}).execute()
177 migratearv.users().update(uuid=old_user_uuid, body={"user": {"username": username}}).execute()
178 except arvados.errors.ApiError as e:
179 print("(%s) Error updating username of %s to '%s' on %s: %s" % (email, old_user_uuid, username, migratecluster, e))
183 for b in by_email[email].values():
184 if b[2].startswith(userhome):
186 if b[1] != username and b[3] == userhome:
187 print("(%s) Cannot migrate %s, conflicting usernames %s and %s" % (email, old_user_uuid, b[1], username))
192 if len(candidates) == 0:
193 if len(userhome) == 5 and userhome not in clusters:
194 print("(%s) Cannot migrate %s, unknown home cluster %s (typo?)" % (email, old_user_uuid, userhome))
196 print("(%s) No user listed with same email to migrate %s to %s, will create new user with username '%s'" % (email, old_user_uuid, userhome, username))
198 newhomecluster = userhome[0:5]
199 homearv = clusters[userhome]
202 conflicts = homearv.users().list(filters=[["username", "=", username]]).execute()
203 if conflicts["items"]:
204 homearv.users().update(uuid=conflicts["items"][0]["uuid"], body={"user": {"username": username+"migrate"}}).execute()
205 user = homearv.users().create(body={"user": {"email": email, "username": username}}).execute()
206 except arvados.errors.ApiError as e:
207 print("(%s) Could not create user: %s" % (email, str(e)))
210 tup = (email, username, user["uuid"], userhome)
213 tup = (email, username, "%s-tpzed-xfakexfakexfake" % (userhome[0:5]), userhome)
214 by_email[email][tup[2]] = tup
215 candidates.append(tup)
216 if len(candidates) > 1:
217 print("(%s) Multiple users listed to migrate %s to %s, use full uuid" % (email, old_user_uuid, userhome))
219 new_user_uuid = candidates[0][2]
221 # cluster where the migration is happening
222 for arv in clusters.values():
223 migratecluster = arv._rootDesc["uuidPrefix"]
224 migratearv = clusters[migratecluster]
226 # the user's new home cluster
227 newhomecluster = userhome[0:5]
228 homearv = clusters[newhomecluster]
230 # create a token for the new user and salt it for the
231 # migration cluster, then use it to access the migration
232 # cluster as the new user once before merging to ensure
233 # the new user is known on that cluster.
236 newtok = homearv.api_client_authorizations().create(body={
237 "api_client_authorization": {'owner_uuid': new_user_uuid}}).execute()
239 newtok = {"uuid": "dry-run", "api_token": "12345"}
240 except arvados.errors.ApiError as e:
241 print("(%s) Could not create API token for %s: %s" % (email, new_user_uuid, e))
244 salted = 'v2/' + newtok["uuid"] + '/' + hmac.new(newtok["api_token"].encode(),
245 msg=migratecluster.encode(),
246 digestmod='sha1').hexdigest()
248 ru = urllib.parse.urlparse(migratearv._rootDesc["rootUrl"])
250 newuser = arvados.api(host=ru.netloc, token=salted, insecure=os.environ.get("ARVADOS_API_HOST_INSECURE")).users().current().execute()
252 newuser = {"is_active": True, "username": username}
253 except arvados.errors.ApiError as e:
254 print("(%s) Error getting user info for %s from %s: %s" % (email, new_user_uuid, migratecluster, e))
258 olduser = migratearv.users().get(uuid=old_user_uuid).execute()
259 except arvados.errors.ApiError as e:
260 if e.resp.status != 404:
261 print("(%s) Could not retrieve user %s from %s, user may have already been migrated: %s" % (email, old_user_uuid, migratecluster, e))
264 if not newuser["is_active"]:
265 print("(%s) Activating user %s on %s" % (email, new_user_uuid, migratecluster))
268 migratearv.users().update(uuid=new_user_uuid, body={"is_active": True}).execute()
269 except arvados.errors.ApiError as e:
270 print("(%s) Could not activate user %s on %s: %s" % (email, new_user_uuid, migratecluster, e))
273 if olduser["is_admin"] and not newuser["is_admin"]:
274 print("(%s) Not migrating %s because user is admin but target user %s is not admin on %s" % (email, old_user_uuid, new_user_uuid, migratecluster))
277 print("(%s) Migrating %s to %s on %s" % (email, old_user_uuid, new_user_uuid, migratecluster))
281 grp = migratearv.groups().create(body={
282 "owner_uuid": new_user_uuid,
283 "name": "Migrated from %s (%s)" % (email, old_user_uuid),
284 "group_class": "project"
285 }, ensure_unique_name=True).execute()
286 migratearv.users().merge(old_user_uuid=old_user_uuid,
287 new_user_uuid=new_user_uuid,
288 new_owner_uuid=grp["uuid"],
289 redirect_to_new_user=True).execute()
290 except arvados.errors.ApiError as e:
291 print("(%s) Error migrating user: %s" % (email, e))
293 if newuser['username'] != username:
296 conflicts = migratearv.users().list(filters=[["username", "=", username]]).execute()
297 if conflicts["items"]:
298 migratearv.users().update(uuid=conflicts["items"][0]["uuid"], body={"user": {"username": username+"migrate"}}).execute()
299 migratearv.users().update(uuid=new_user_uuid, body={"user": {"username": username}}).execute()
300 except arvados.errors.ApiError as e:
301 print("(%s) Error updating username of %s to '%s' on %s: %s" % (email, new_user_uuid, username, migratecluster, e))
303 if __name__ == "__main__":