2 # Copyright (C) The Arvados Authors. All rights reserved.
4 # SPDX-License-Identifier: Apache-2.0
17 parser = argparse.ArgumentParser(description='Migrate users to federated identity, see https://doc.arvados.org/admin/merge-remote-account.html')
18 parser.add_argument('--tokens', type=str, required=True)
19 group = parser.add_mutually_exclusive_group(required=True)
20 group.add_argument('--report', type=str, help="Generate report .csv file listing users by email address and their associated Arvados accounts")
21 group.add_argument('--migrate', type=str, help="Consume report .csv and migrate users to designated Arvados accounts")
22 group.add_argument('--check', action="store_true", help="Check that tokens are usable and the federation is well connected")
23 args = parser.parse_args()
27 print("Reading %s" % args.tokens)
28 with open(args.tokens, "rt") as f:
29 for r in csv.reader(f):
32 print("Contacting %s" % (host))
33 arv = arvados.api(host=host, token=token, cache=False)
35 cur = arv.users().current().execute()
36 arv.api_client_authorizations().list(limit=1).execute()
37 except arvados.errors.ApiError as e:
38 errors.append("checking token for %s: %s" % (host, e))
39 errors.append(' This script requires a token issued to a trusted client in order to manipulate access tokens.')
40 errors.append(' See "Trusted client setting" in https://doc.arvados.org/install/install-workbench-app.html')
41 errors.append(' and https://doc.arvados.org/api/tokens.html')
44 if not cur["is_admin"]:
45 errors.append("Not admin of %s" % host)
48 clusters[arv._rootDesc["uuidPrefix"]] = arv
51 print("Checking that the federation is well connected")
52 for v in clusters.values():
54 if r != v._rootDesc["uuidPrefix"] and r not in v._rootDesc["remoteHosts"]:
55 errors.append("%s is missing from remoteHosts of %s" % (r, v._rootDesc["uuidPrefix"]))
56 for r in v._rootDesc["remoteHosts"]:
57 if r != "*" and r not in clusters:
58 print("WARNING: %s is federated with %s but %s is missing from the tokens file or the token is invalid" % (v._rootDesc["uuidPrefix"], r, r))
62 print("ERROR: "+str(e))
66 print("Tokens file passed checks")
71 for c, arv in clusters.items():
72 print("Getting user list from %s" % c)
73 ul = arvados.util.list_all(arv.users().list)
75 if l["uuid"].startswith(c):
78 out = csv.writer(open(args.report, "wt"))
80 out.writerow(("email", "user uuid", "primary cluster/user"))
82 users = sorted(users, key=lambda u: u["email"]+"::"+u["uuid"])
87 if u["uuid"].endswith("-anonymouspublic") or u["uuid"].endswith("-000000000000000"):
90 lastemail = u["email"]
91 if u["email"] == lastemail:
98 if a["uuid"] != homeuuid:
101 out.writerow((a["email"], a["uuid"], homeuuid[0:5]))
102 lastemail = u["email"]
109 if a["uuid"] != homeuuid:
112 out.writerow((a["email"], a["uuid"], homeuuid[0:5]))
114 print("Wrote %s" % args.report)
119 with open(args.migrate, "rt") as f:
120 for r in csv.reader(f):
123 by_email.setdefault(r[0], [])
124 by_email[r[0]].append(r)
132 print("(%s) Skipping %s, no home cluster specified" % (email, old_user_uuid))
133 if old_user_uuid.startswith(userhome):
136 for b in by_email[email]:
137 if b[1].startswith(userhome):
139 if len(candidates) == 0:
140 if len(userhome) == 5 and userhome not in clusters:
141 print("(%s) Cannot migrate %s, unknown home cluster %s (typo?)" % (email, old_user_uuid, userhome))
143 print("(%s) No user listed with same email to migrate %s to %s" % (email, old_user_uuid, userhome))
145 if len(candidates) > 1:
146 print("(%s) Multiple users listed to migrate %s to %s, use full uuid" % (email, old_user_uuid, userhome))
148 new_user_uuid = candidates[0][1]
150 # cluster where the migration is happening
151 migratecluster = old_user_uuid[0:5]
152 migratearv = clusters[migratecluster]
154 # the user's new home cluster
155 newhomecluster = userhome[0:5]
156 homearv = clusters[newhomecluster]
158 # create a token for the new user and salt it for the
159 # migration cluster, then use it to access the migration
160 # cluster as the new user once before merging to ensure
161 # the new user is known on that cluster.
163 newtok = homearv.api_client_authorizations().create(body={
164 "api_client_authorization": {'owner_uuid': new_user_uuid}}).execute()
165 except arvados.errors.ApiError as e:
166 print("(%s) Could not create API token for %s: %s" % (email, new_user_uuid, e))
169 salted = 'v2/' + newtok["uuid"] + '/' + hmac.new(newtok["api_token"].encode(),
170 msg=migratecluster.encode(),
171 digestmod='sha1').hexdigest()
173 ru = urllib.parse.urlparse(migratearv._rootDesc["rootUrl"])
174 newuser = arvados.api(host=ru.netloc, token=salted).users().current().execute()
175 except arvados.errors.ApiError as e:
176 print("(%s) Error getting user info for %s from %s: %s" % (email, new_user_uuid, migratecluster, e))
180 olduser = migratearv.users().get(uuid=old_user_uuid).execute()
181 except arvados.errors.ApiError as e:
182 print("(%s) Could not retrieve user %s from %s, user may have already been migrated: %s" % (email, old_user_uuid, migratecluster, e))
185 if not newuser["is_active"]:
186 print("(%s) Activating user %s on %s" % (email, new_user_uuid, migratecluster))
188 migratearv.users().update(uuid=new_user_uuid, body={"is_active": True}).execute()
189 except arvados.errors.ApiError as e:
190 print("(%s) Could not activate user %s on %s: %s" % (email, new_user_uuid, migratecluster, e))
193 if olduser["is_admin"] and not newuser["is_admin"]:
194 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))
197 print("(%s) Migrating %s to %s on %s" % (email, old_user_uuid, new_user_uuid, migratecluster))
200 grp = migratearv.groups().create(body={
201 "owner_uuid": new_user_uuid,
202 "name": "Migrated from %s (%s)" % (email, old_user_uuid),
203 "group_class": "project"
204 }, ensure_unique_name=True).execute()
205 migratearv.users().merge(old_user_uuid=old_user_uuid,
206 new_user_uuid=new_user_uuid,
207 new_owner_uuid=grp["uuid"],
208 redirect_to_new_user=True).execute()
209 except arvados.errors.ApiError as e:
210 print("(%s) Error migrating user: %s" % (email, e))
212 if __name__ == "__main__":