Path: blob/master/gen_sa_accounts.py
1643 views
from argparse import ArgumentParser1from base64 import b64decode2from errno import EEXIST3from glob import glob4from json import loads5from os import mkdir, path as ospath6from pickle import dump, load7from random import choice8from sys import exit9from time import sleep1011from google.auth.transport.requests import Request12from google_auth_oauthlib.flow import InstalledAppFlow13from googleapiclient.discovery import build14from googleapiclient.errors import HttpError1516SCOPES = [17"https://www.googleapis.com/auth/drive",18"https://www.googleapis.com/auth/cloud-platform",19"https://www.googleapis.com/auth/iam",20]21project_create_ops = []22current_key_dump = []23sleep_time = 3024CHARS = "-abcdefghijklmnopqrstuvwxyz1234567890"252627def _create_accounts(service, project, count):28batch = service.new_batch_http_request(callback=_def_batch_resp)29for _ in range(count):30aid = _generate_id("mfc-")31batch.add(32service.projects()33.serviceAccounts()34.create(35name=f"projects/{project}",36body={37"accountId": aid,38"serviceAccount": {"displayName": aid},39},40)41)42try:43batch.execute()44except HttpError as e:45print("Error creating accounts:", e)464748def _create_remaining_accounts(iam, project):49print(f"Creating accounts in {project}")50sa_count = len(_list_sas(iam, project))51while sa_count != 100:52_create_accounts(iam, project, 100 - sa_count)53sa_count = len(_list_sas(iam, project))545556def _generate_id(prefix="saf-"):57return prefix + "".join(choice(CHARS) for _ in range(25)) + choice(CHARS[1:])585960def _get_projects(service):61try:62return [i["projectId"] for i in service.projects().list().execute()["projects"]]63except HttpError as e:64print("Error fetching projects:", e)65return []666768def _def_batch_resp(id, resp, exception):69if exception is not None:70if str(exception).startswith("<HttpError 429"):71sleep(sleep_time / 100)72else:73print("Batch error:", exception)747576def _pc_resp(id, resp, exception):77global project_create_ops78if exception is not None:79print("Project creation error:", exception)80else:81for i in resp.values():82project_create_ops.append(i)838485def _create_projects(cloud, count):86global project_create_ops87batch = cloud.new_batch_http_request(callback=_pc_resp)88new_projs = []89for _ in range(count):90new_proj = _generate_id()91new_projs.append(new_proj)92batch.add(cloud.projects().create(body={"project_id": new_proj}))93try:94batch.execute()95except HttpError as e:96print("Error creating projects:", e)97return []9899for op in project_create_ops:100while True:101try:102resp = cloud.operations().get(name=op).execute()103if resp.get("done"):104break105except HttpError as e:106print("Error fetching operation status:", e)107break108sleep(3)109return new_projs110111112def _enable_services(service, projects, ste):113batch = service.new_batch_http_request(callback=_def_batch_resp)114for project in projects:115for s in ste:116batch.add(117service.services().enable(name=f"projects/{project}/services/{s}")118)119try:120batch.execute()121except HttpError as e:122print("Error enabling services:", e)123124125def _list_sas(iam, project):126try:127resp = (128iam.projects()129.serviceAccounts()130.list(name=f"projects/{project}", pageSize=100)131.execute()132)133return resp.get("accounts", [])134except HttpError as e:135print("Error listing service accounts:", e)136return []137138139def _batch_keys_resp(id, resp, exception):140global current_key_dump141if exception is not None:142current_key_dump = None143sleep(sleep_time / 100)144elif current_key_dump is None:145sleep(sleep_time / 100)146else:147try:148key_name = resp["name"][resp["name"].rfind("/") :]149key_data = b64decode(resp["privateKeyData"]).decode("utf-8")150current_key_dump.append((key_name, key_data))151except Exception as e:152print("Error processing key response:", e)153154155def _create_sa_keys(iam, projects, path_dir):156global current_key_dump157for project in projects:158current_key_dump = []159print(f"Downloading keys from {project}")160while current_key_dump is None or len(current_key_dump) != 100:161batch = iam.new_batch_http_request(callback=_batch_keys_resp)162total_sas = _list_sas(iam, project)163for sa in total_sas:164batch.add(165iam.projects()166.serviceAccounts()167.keys()168.create(169name=f"projects/{project}/serviceAccounts/{sa['uniqueId']}",170body={171"privateKeyType": "TYPE_GOOGLE_CREDENTIALS_FILE",172"keyAlgorithm": "KEY_ALG_RSA_2048",173},174)175)176try:177batch.execute()178except HttpError as e:179print("Error creating SA keys:", e)180current_key_dump = None181182if current_key_dump is None:183print(f"Redownloading keys from {project}")184current_key_dump = []185else:186for index, key in enumerate(current_key_dump):187try:188with open(f"{path_dir}/{index}.json", "w+") as f:189f.write(key[1])190except IOError as e:191print(f"Error writing key file {index}.json:", e)192193194def _delete_sas(iam, project):195sas = _list_sas(iam, project)196batch = iam.new_batch_http_request(callback=_def_batch_resp)197for account in sas:198batch.add(iam.projects().serviceAccounts().delete(name=account["name"]))199try:200batch.execute()201except HttpError as e:202print("Error deleting service accounts:", e)203204205def serviceaccountfactory(206credentials="credentials.json",207token="token_sa.pickle",208path=None,209list_projects=False,210list_sas=None,211create_projects=None,212max_projects=12,213enable_services=None,214services=None,215create_sas=None,216delete_sas=None,217download_keys=None,218):219if services is None:220services = ["iam", "drive"]221selected_projects = []222try:223proj_id = loads(open(credentials, "r").read())["installed"]["project_id"]224except Exception as e:225exit(f"Error reading credentials file: {str(e)}")226227creds = None228if path and not path.endswith("/"):229path = path.rstrip("/")230231if path and path != "accounts":232try:233mkdir(path)234except OSError as e:235if e.errno != EEXIST:236print("Error creating output directory:", e)237exit(1)238239if ospath.exists(token):240try:241with open(token, "rb") as t:242creds = load(t)243except Exception as e:244print("Error loading token file:", e)245if not creds or not creds.valid:246try:247if creds and creds.expired and creds.refresh_token:248creds.refresh(Request())249else:250flow = InstalledAppFlow.from_client_secrets_file(credentials, SCOPES)251creds = flow.run_local_server(port=0, open_browser=False)252with open(token, "wb") as t:253dump(creds, t)254except Exception as e:255exit(f"Error obtaining credentials: {str(e)}")256257try:258cloud = build("cloudresourcemanager", "v1", credentials=creds)259iam = build("iam", "v1", credentials=creds)260serviceusage = build("serviceusage", "v1", credentials=creds)261except Exception as e:262exit(f"Error building service clients: {str(e)}")263264projs = None265while projs is None:266try:267projs = _get_projects(cloud)268except HttpError:269try:270serviceusage.services().enable(271name=f"projects/{proj_id}/services/cloudresourcemanager.googleapis.com"272).execute()273except HttpError as ee:274print("Error enabling cloudresourcemanager:", ee)275input("Press Enter to retry.")276if list_projects:277return _get_projects(cloud)278if list_sas:279return _list_sas(iam, list_sas)280if create_projects:281print(f"Creating projects: {create_projects}")282if create_projects > 0:283current_count = len(_get_projects(cloud))284if current_count + create_projects <= max_projects:285print("Creating %d projects" % create_projects)286nprjs = _create_projects(cloud, create_projects)287selected_projects = nprjs288else:289exit(290"Cannot create %d new project(s).\n"291"Please reduce the value or delete existing projects.\n"292"Max projects allowed: %d, already in use: %d"293% (create_projects, max_projects, current_count)294)295else:296print("Overwriting all service accounts in existing projects.")297input("Press Enter to continue...")298299if enable_services:300target = [enable_services]301if enable_services == "~":302target = selected_projects303elif enable_services == "*":304target = _get_projects(cloud)305service_list = [f"{s}.googleapis.com" for s in services]306print("Enabling services")307_enable_services(serviceusage, target, service_list)308if create_sas:309target = [create_sas]310if create_sas == "~":311target = selected_projects312elif create_sas == "*":313target = _get_projects(cloud)314for proj in target:315_create_remaining_accounts(iam, proj)316if download_keys:317target = [download_keys]318if download_keys == "~":319target = selected_projects320elif download_keys == "*":321target = _get_projects(cloud)322_create_sa_keys(iam, target, path)323if delete_sas:324target = [delete_sas]325if delete_sas == "~":326target = selected_projects327elif delete_sas == "*":328target = _get_projects(cloud)329for proj in target:330print(f"Deleting service accounts in {proj}")331_delete_sas(iam, proj)332333334if __name__ == "__main__":335parse = ArgumentParser(description="A tool to create Google service accounts.")336parse.add_argument(337"--path",338"-p",339default="accounts",340help="Specify an alternate directory to output the credential files.",341)342parse.add_argument("--token", default="token_sa.pickle", help="Token file path.")343parse.add_argument(344"--credentials",345default="credentials.json",346help="Credentials file path.",347)348parse.add_argument(349"--list-projects",350default=False,351action="store_true",352help="List projects viewable by the user.",353)354parse.add_argument(355"--list-sas", default=False, help="List service accounts in a project."356)357parse.add_argument(358"--create-projects", type=int, default=None, help="Creates up to N projects."359)360parse.add_argument(361"--max-projects",362type=int,363default=12,364help="Max projects allowed. Default: 12",365)366parse.add_argument(367"--enable-services",368default=None,369help="Enables services on the project. Default: IAM and Drive",370)371parse.add_argument(372"--services",373nargs="+",374default=["iam", "drive"],375help="Specify a different set of services to enable.",376)377parse.add_argument(378"--create-sas", default=None, help="Create service accounts in a project."379)380parse.add_argument(381"--delete-sas", default=None, help="Delete service accounts in a project."382)383parse.add_argument(384"--download-keys",385default=None,386help="Download keys for service accounts in a project.",387)388parse.add_argument(389"--quick-setup",390default=None,391type=int,392help="Create projects, enable services, create SAs and download keys.",393)394parse.add_argument(395"--new-only",396default=False,397action="store_true",398help="Do not use existing projects.",399)400args = parse.parse_args()401402if not ospath.exists(args.credentials):403options = glob("*.json")404print(405f"No credentials found at {args.credentials}. Please enable the Drive API and save the JSON as {args.credentials}."406)407if not options:408exit("No available credential files found.")409else:410print("Select a credentials file:")411for idx, opt in enumerate(options):412print(f" {idx + 1}) {opt}")413while True:414inp = input("> ")415try:416choice_idx = int(inp) - 1417if 0 <= choice_idx < len(options):418args.credentials = options[choice_idx]419break420except ValueError:421if inp in options:422args.credentials = inp423break424print(425f"Use --credentials {args.credentials} next time to use this credentials file."426)427if args.quick_setup:428opt = "~" if args.new_only else "*"429args.services = ["iam", "drive"]430args.create_projects = args.quick_setup431args.enable_services = opt432args.create_sas = opt433args.download_keys = opt434resp = serviceaccountfactory(435path=args.path,436token=args.token,437credentials=args.credentials,438list_projects=args.list_projects,439list_sas=args.list_sas,440create_projects=args.create_projects,441max_projects=args.max_projects,442create_sas=args.create_sas,443delete_sas=args.delete_sas,444enable_services=args.enable_services,445services=args.services,446download_keys=args.download_keys,447)448if resp is not None:449if args.list_projects:450if resp:451print("Projects (%d):" % len(resp))452for proj in resp:453print(f" {proj}")454else:455print("No projects found.")456elif args.list_sas:457if resp:458print("Service accounts in %s (%d):" % (args.list_sas, len(resp)))459for sa in resp:460print(f" {sa['email']} ({sa['uniqueId']})")461else:462print("No service accounts found.")463464465