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 for k,v in rh.items():
43 arv = arvados.api(host=v, token=os.environ["ARVADOS_API_TOKEN"], cache=False)
44 config = arv.configs().get().execute()
45 if config["Login"]["LoginCluster"] != "" and loginCluster is None:
46 loginCluster = config["Login"]["LoginCluster"]
49 print("Checking that the federation is well connected")
50 for arv in clusters.values():
51 config = arv.configs().get().execute()
52 if loginCluster and config["Login"]["LoginCluster"] != loginCluster and config["ClusterID"] != loginCluster:
53 errors.append("Inconsistent login cluster configuration, expected '%s' on %s but was '%s'" % (loginCluster, config["ClusterID"], config["Login"]["LoginCluster"]))
56 cur = arv.users().current().execute()
57 #arv.api_client_authorizations().list(limit=1).execute()
58 except arvados.errors.ApiError as e:
59 errors.append("checking token for %s %s" % (arv._rootDesc["rootUrl"], e))
60 errors.append(' This script requires a token issued to a trusted client in order to manipulate access tokens.')
61 errors.append(' See "Trusted client setting" in https://doc.arvados.org/install/install-workbench-app.html')
62 errors.append(' and https://doc.arvados.org/api/tokens.html')
65 if not cur["is_admin"]:
66 errors.append("Not admin of %s" % host)
70 if r != arv._rootDesc["uuidPrefix"] and r not in arv._rootDesc["remoteHosts"]:
71 errors.append("%s is missing from remoteHosts of %s" % (r, arv._rootDesc["uuidPrefix"]))
72 for r in arv._rootDesc["remoteHosts"]:
73 if r != "*" and r not in clusters:
74 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))
78 print("ERROR: "+str(e))
82 print("Tokens file passed checks")
87 for c, arv in clusters.items():
88 print("Getting user list from %s" % c)
89 ul = arvados.util.list_all(arv.users().list)
91 if l["uuid"].startswith(c):
94 out = csv.writer(open(args.report, "wt"))
96 out.writerow(("email", "username", "user uuid", "home cluster"))
98 users = sorted(users, key=lambda u: u["email"]+"::"+(u["username"] or "")+"::"+u["uuid"])
103 if u["uuid"].endswith("-anonymouspublic") or u["uuid"].endswith("-000000000000000"):
105 if lastemail == None:
106 lastemail = u["email"]
107 if u["email"] == lastemail:
114 if a["uuid"] != homeuuid:
117 out.writerow((a["email"], a["username"], a["uuid"], loginCluster or homeuuid[0:5]))
118 lastemail = u["email"]
125 if a["uuid"] != homeuuid:
128 out.writerow((a["email"], a["username"], a["uuid"], loginCluster or homeuuid[0:5]))
130 print("Wrote %s" % args.report)
132 if args.migrate or args.dry_run:
134 print("Performing dry run")
138 with open(args.migrate or args.dry_run, "rt") as f:
139 for r in csv.reader(f):
142 by_email.setdefault(r[0], [])
143 by_email[r[0]].append(r)
152 print("(%s) Skipping %s, no home cluster specified" % (email, old_user_uuid))
153 if old_user_uuid.startswith(userhome):
156 for b in by_email[email]:
157 if b[2].startswith(userhome):
159 if len(candidates) == 0:
160 if len(userhome) == 5 and userhome not in clusters:
161 print("(%s) Cannot migrate %s, unknown home cluster %s (typo?)" % (email, old_user_uuid, userhome))
163 print("(%s) No user listed with same email to migrate %s to %s, will create new user" % (email, old_user_uuid, userhome))
165 newhomecluster = userhome[0:5]
166 homearv = clusters[userhome]
167 user = homearv.users().create({"email": email, "username": username}).execute()
168 candidates.append((email, username, user["uuid"], userhome))
170 candidates.append((email, username, "%s-tpzed-xfakexfakexfake" % (userhome[0:5]), userhome))
171 if len(candidates) > 1:
172 print("(%s) Multiple users listed to migrate %s to %s, use full uuid" % (email, old_user_uuid, userhome))
174 new_user_uuid = candidates[0][2]
176 # cluster where the migration is happening
177 for arv in clusters.values():
178 migratecluster = arv._rootDesc["uuidPrefix"]
179 migratearv = clusters[migratecluster]
181 # the user's new home cluster
182 newhomecluster = userhome[0:5]
183 homearv = clusters[newhomecluster]
185 # create a token for the new user and salt it for the
186 # migration cluster, then use it to access the migration
187 # cluster as the new user once before merging to ensure
188 # the new user is known on that cluster.
191 newtok = homearv.api_client_authorizations().create(body={
192 "api_client_authorization": {'owner_uuid': new_user_uuid}}).execute()
194 newtok = {"uuid": "dry-run", "api_token": "12345"}
195 except arvados.errors.ApiError as e:
196 print("(%s) Could not create API token for %s: %s" % (email, new_user_uuid, e))
199 salted = 'v2/' + newtok["uuid"] + '/' + hmac.new(newtok["api_token"].encode(),
200 msg=migratecluster.encode(),
201 digestmod='sha1').hexdigest()
203 ru = urllib.parse.urlparse(migratearv._rootDesc["rootUrl"])
205 newuser = arvados.api(host=ru.netloc, token=salted).users().current().execute()
207 newuser = {"is_active": True}
208 except arvados.errors.ApiError as e:
209 print("(%s) Error getting user info for %s from %s: %s" % (email, new_user_uuid, migratecluster, e))
213 olduser = migratearv.users().get(uuid=old_user_uuid).execute()
214 except arvados.errors.ApiError as e:
215 if e.resp.status != 404:
216 print("(%s) Could not retrieve user %s from %s, user may have already been migrated: %s" % (email, old_user_uuid, migratecluster, e))
219 if not newuser["is_active"]:
220 print("(%s) Activating user %s on %s" % (email, new_user_uuid, migratecluster))
223 migratearv.users().update(uuid=new_user_uuid, body={"is_active": True}).execute()
224 except arvados.errors.ApiError as e:
225 print("(%s) Could not activate user %s on %s: %s" % (email, new_user_uuid, migratecluster, e))
228 if olduser["is_admin"] and not newuser["is_admin"]:
229 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))
233 print("(%s) Migrating %s to %s on %s" % (email, old_user_uuid, new_user_uuid, migratecluster))
237 grp = migratearv.groups().create(body={
238 "owner_uuid": new_user_uuid,
239 "name": "Migrated from %s (%s)" % (email, old_user_uuid),
240 "group_class": "project"
241 }, ensure_unique_name=True).execute()
242 migratearv.users().merge(old_user_uuid=old_user_uuid,
243 new_user_uuid=new_user_uuid,
244 new_owner_uuid=grp["uuid"],
245 redirect_to_new_user=True).execute()
246 except arvados.errors.ApiError as e:
247 print("(%s) Error migrating user: %s" % (email, e))
249 if __name__ == "__main__":