diff --git a/src/.gitignore b/src/.gitignore index 125d06e3..38305410 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -64,7 +64,6 @@ nobrowser.txt nocache.txt noverifyssl.txt gamcache/ -gam/ gam-64/ *.zip *.msi diff --git a/src/gam.py b/src/gam.py index cb146c39..16fd9044 100755 --- a/src/gam.py +++ b/src/gam.py @@ -1,11609 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# -# GAM -# -# Copyright 2019, LLC All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""GAM is a command line tool which allows Administrators to control their G Suite domain and accounts. +"""Provides backwards compatibility for calling gam as a single .py file""" -With GAM you can programmatically create users, turn on/off services for users like POP and Forwarding and much more. -For more information, see https://git.io/gam -""" - -import base64 -import configparser -import csv -import datetime -import difflib -from email import message_from_string -import hashlib -import io -import json -import mimetypes -import os -import pkg_resources -import platform -import random -from secrets import SystemRandom -import re -import shlex -import signal -import socket -import ssl -import struct import sys -import time -import uuid -import webbrowser -import zipfile -import http.client as http_client -from multiprocessing import Pool as mp_pool -from multiprocessing import freeze_support as mp_freeze_support -from multiprocessing import set_start_method as mp_set_start_method -from urllib.parse import quote, urlencode, urlparse -import dateutil.parser -import googleapiclient -import googleapiclient.discovery -import googleapiclient.errors -import googleapiclient.http -import google.oauth2.service_account -import httplib2 - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509.oid import NameOID - -import auth.oauth -import controlflow -import display -import fileutils -import gapi.calendar -import gapi.directory -import gapi.directory.cros -import gapi.directory.customer -import gapi.directory.resource -import gapi.errors -import gapi.reports -import gapi.storage -import gapi.vault -import gapi -import transport -import utils -from var import * - -if platform.system() == 'Windows': - # No crypt module on Win, use passlib - from passlib.hash import sha512_crypt -else: - from crypt import crypt - -if platform.system() == 'Linux': - import distro - -# Finding path method varies between Python source, PyInstaller and StaticX -if os.environ.get('STATICX_PROG_PATH', False): - # StaticX static executable - GM_Globals[GM_GAM_PATH] = os.path.dirname(os.environ['STATICX_PROG_PATH']) - GM_Globals[GM_GAM_TYPE] = 'staticx' - # Pyinstaller executable -elif getattr(sys, 'frozen', False): - GM_Globals[GM_GAM_PATH] = os.path.dirname(sys.executable) - GM_Globals[GM_GAM_TYPE] = 'pyinstaller' -else: - # Source code - GM_Globals[GM_GAM_PATH] = os.path.dirname(os.path.realpath(__file__)) - GM_Globals[GM_GAM_TYPE] = 'pythonsource' - -def showUsage(): - doGAMVersion(checkForArgs=False) - print(''' -Usage: gam [OPTIONS]... - -GAM. Retrieve or set G Suite domain, -user, group and alias settings. Exhaustive list of commands -can be found at: https://github.com/jay0lee/GAM/wiki - -Examples: -gam info domain -gam create user jsmith firstname John lastname Smith password secretpass -gam update user jsmith suspended on -gam.exe update group announcements add member jsmith -... - -''') - -def currentCount(i, count): - return f' ({i}/{count})' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '' - -def currentCountNL(i, count): - return f' ({i}/{count})\n' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '\n' - -def printGettingAllItems(items, query): - if query: - sys.stderr.write(f"Getting all {items} in G Suite account that match query ({query}) (may take some time on a large account)...\n") - else: - sys.stderr.write(f"Getting all {items} in G Suite account (may take some time on a large account)...\n") - -def entityServiceNotApplicableWarning(entityType, entityName, i, count): - sys.stderr.write(f'{entityType}: {entityName}, Service not applicable/Does not exist{currentCountNL(i, count)}') - -def entityDoesNotExistWarning(entityType, entityName, i, count): - sys.stderr.write(f'{entityType}: {entityName}, Does not exist{currentCountNL(i, count)}') - -def entityUnknownWarning(entityType, entityName, i, count): - domain = getEmailAddressDomain(entityName) - if (domain == GC_Values[GC_DOMAIN]) or (domain.endswith('google.com')): - entityDoesNotExistWarning(entityType, entityName, i, count) - else: - entityServiceNotApplicableWarning(entityType, entityName, i, count) - -def printLine(message): - sys.stdout.write(message+'\n') - -def getBoolean(value, item): - value = value.lower() - if value in true_values: - return True - if value in false_values: - return False - controlflow.system_error_exit(2, f'Value for {item} must be {"|".join(true_values)} or {"|".join(false_values)}; got {value}') - -def getCharSet(i): - if (i == len(sys.argv)) or (sys.argv[i].lower() != 'charset'): - return (i, GC_Values.get(GC_CHARSET, GM_Globals[GM_SYS_ENCODING])) - return (i+2, sys.argv[i+1]) - -def supportsColoredText(): - """Determines if the current terminal environment supports colored text. - - Returns: - Bool, True if the current terminal environment supports colored text via - ANSI escape characters. - """ - # Make a rudimentary check for Windows. Though Windows does seem to support - # colorization with VT100 emulation, it is disabled by default. Therefore, - # we'll simply disable it in GAM on Windows for now. - return not GM_Globals[GM_WINDOWS] - -def createColoredText(text, color): - """Uses ANSI escape characters to create colored text in supported terminals. - - See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors - - Args: - text: String, The text to colorize using ANSI escape characters. - color: String, An ANSI escape sequence denoting the color of the text to be - created. See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors - - Returns: - The input text with appropriate ANSI escape characters to create - colorization in a supported terminal environment. - """ - END_COLOR_SEQUENCE = '\033[0m' # Ends the applied color formatting - if supportsColoredText(): - return color + text + END_COLOR_SEQUENCE - return text # Hand back the plain text, uncolorized. - -def createRedText(text): - """Uses ANSI encoding to create red colored text if supported.""" - return createColoredText(text, '\033[91m') - -def createGreenText(text): - """Uses ANSI encoding to create green colored text if supported.""" - return createColoredText(text, '\u001b[32m') - -def createYellowText(text): - """Uses ANSI encoding to create yellow text if supported.""" - return createColoredText(text, '\u001b[33m') - -COLORHEX_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$') - -def getColor(color): - color = color.lower().strip() - if color in WEBCOLOR_MAP: - return WEBCOLOR_MAP[color] - tg = COLORHEX_PATTERN.match(color) - if tg: - return tg.group(0) - controlflow.system_error_exit(2, f'A color must be a valid name or # and six hex characters (#012345); got {color}') - -def getLabelColor(color): - color = color.lower().strip() - tg = COLORHEX_PATTERN.match(color) - if tg: - color = tg.group(0) - if color in LABEL_COLORS: - return color - controlflow.expected_argument_exit("label color", ", ".join(LABEL_COLORS), color) - controlflow.system_error_exit(2, f'A label color must be # and six hex characters (#012345); got {color}') - -def getInteger(value, item, minVal=None, maxVal=None): - try: - number = int(value.strip()) - if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)): - return number - except ValueError: - pass - controlflow.system_error_exit(2, f'expected {item} in range <{utils.integerLimits(minVal, maxVal)}>, got {value}') - -def removeCourseIdScope(courseId): - if courseId.startswith('d:'): - return courseId[2:] - return courseId - -def addCourseIdScope(courseId): - if not courseId.isdigit() and courseId[:2] != 'd:': - return f'd:{courseId}' - return courseId - -# Get domain from email address -def getEmailAddressDomain(emailAddress): - atLoc = emailAddress.find('@') - if atLoc == -1: - return GC_Values[GC_DOMAIN].lower() - return emailAddress[atLoc+1:].lower() - -# Split email address unto user and domain -def splitEmailAddress(emailAddress): - atLoc = emailAddress.find('@') - if atLoc == -1: - return (emailAddress.lower(), GC_Values[GC_DOMAIN].lower()) - return (emailAddress[:atLoc].lower(), emailAddress[atLoc+1:].lower()) - -# Normalize user/group email address/uid -# uid:12345abc -> 12345abc -# foo -> foo@domain -# foo@ -> foo@domain -# foo@bar.com -> foo@bar.com -# @domain -> domain -def normalizeEmailAddressOrUID(emailAddressOrUID, noUid=False, checkForCustomerId=False, noLower=False): - if checkForCustomerId and (emailAddressOrUID == GC_Values[GC_CUSTOMER_ID]): - return emailAddressOrUID - if not noUid: - cg = UID_PATTERN.match(emailAddressOrUID) - if cg: - return cg.group(1) - atLoc = emailAddressOrUID.find('@') - if atLoc == 0: - return emailAddressOrUID[1:].lower() if not noLower else emailAddressOrUID[1:] - if (atLoc == -1) or (atLoc == len(emailAddressOrUID)-1) and GC_Values[GC_DOMAIN]: - if atLoc == -1: - emailAddressOrUID = f'{emailAddressOrUID}@{GC_Values[GC_DOMAIN]}' - else: - emailAddressOrUID = f'{emailAddressOrUID}{GC_Values[GC_DOMAIN]}' - return emailAddressOrUID.lower() if not noLower else emailAddressOrUID - -# Normalize student/guardian email address/uid -# 12345678 -> 12345678 -# - -> - -# Otherwise, same results as normalizeEmailAddressOrUID -def normalizeStudentGuardianEmailAddressOrUID(emailAddressOrUID): - if emailAddressOrUID.isdigit() or emailAddressOrUID == '-': - return emailAddressOrUID - return normalizeEmailAddressOrUID(emailAddressOrUID) -# -# Set global variables -# Check for GAM updates based on status of noupdatecheck.txt -# -def SetGlobalVariables(): - - def _getOldEnvVar(itemName, envVar): - value = os.environ.get(envVar, GC_Defaults[itemName]) - if GC_VAR_INFO[itemName][GC_VAR_TYPE] == GC_TYPE_INTEGER: - try: - number = int(value) - minVal, maxVal = GC_VAR_INFO[itemName][GC_VAR_LIMITS] - if number < minVal: - number = minVal - elif maxVal and (number > maxVal): - number = maxVal - except ValueError: - number = GC_Defaults[itemName] - value = number - GC_Defaults[itemName] = value - - def _getOldSignalFile(itemName, fileName, filePresentValue=True, fileAbsentValue=False): - GC_Defaults[itemName] = filePresentValue if os.path.isfile(os.path.join(GC_Defaults[GC_CONFIG_DIR], fileName)) else fileAbsentValue - - def _getCfgDirectory(itemName): - return GC_Defaults[itemName] - - def _getCfgFile(itemName): - if not GC_Defaults[itemName]: - return None - value = os.path.expanduser(GC_Defaults[itemName]) - if not os.path.isabs(value): - value = os.path.expanduser(os.path.join(GC_Values[GC_CONFIG_DIR], value)) - return value - - def _getCfgHeaderFilter(itemName): - value = GC_Defaults[itemName] - headerFilters = [] - if not value: - return headerFilters - filters = shlexSplitList(value) - for filterStr in filters: - try: - headerFilters.append(re.compile(filterStr, re.IGNORECASE)) - except re.error as e: - controlflow.system_error_exit(3, f'Item: {itemName}: "{filterStr}", Invalid RE: {str(e)}') - return headerFilters - - ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count)\s*([<>]=?|=|!=)\s*(.+)$', re.IGNORECASE) - ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE) - ROW_FILTER_RE_PATTERN = re.compile(r'^(regex|notregex):(.+)$', re.IGNORECASE) - - def _getCfgRowFilter(itemName): - value = GC_Defaults[itemName] - rowFilters = {} - if not value: - return rowFilters - if value.startswith('{'): - try: - filterDict = json.loads(value.encode('unicode-escape').decode(UTF8)) - except (TypeError, ValueError) as e: - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{value}", Failed to parse as JSON: {str(e)}') - else: - filterDict = {} - status, filterList = shlexSplitListStatus(value) - if not status: - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{value}", Failed to parse as list') - for filterVal in filterList: - if not filterVal: - continue - try: - filterTokens = shlexSplitList(filterVal, ':') - column = filterTokens[0] - filterStr = ':'.join(filterTokens[1:]) - except ValueError: - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{filterVal}", Expected column:filter') - filterDict[column] = filterStr - for column, filterStr in iter(filterDict.items()): - mg = ROW_FILTER_COMP_PATTERN.match(filterStr) - if mg: - if mg.group(1) in ['date', 'time']: - if mg.group(1) == 'date': - valid, filterValue = utils.get_row_filter_date_or_delta_from_now(mg.group(3)) - else: - valid, filterValue = utils.get_row_filter_time_or_delta_from_now(mg.group(3)) - if valid: - rowFilters[column] = (mg.group(1), mg.group(2), filterValue) - continue - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Expected: {filterValue}') - else: #count - if mg.group(3).isdigit(): - rowFilters[column] = (mg.group(1), mg.group(2), int(mg.group(3))) - continue - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Expected: ') - mg = ROW_FILTER_BOOL_PATTERN.match(filterStr) - if mg: - value = mg.group(2).lower() - if value in true_values: - filterValue = True - elif value in false_values: - filterValue = False - else: - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Expected true|false') - rowFilters[column] = (mg.group(1), filterValue) - continue - mg = ROW_FILTER_RE_PATTERN.match(filterStr) - if mg: - try: - rowFilters[column] = (mg.group(1), re.compile(mg.group(2))) - continue - except re.error as e: - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Invalid RE: {str(e)}') - controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": {filterStr}, Expected: (date|time|count) or (boolean:true|false) or (regex|notregex:)') - return rowFilters - - GC_Defaults[GC_CONFIG_DIR] = GM_Globals[GM_GAM_PATH] - GC_Defaults[GC_CACHE_DIR] = os.path.join(GM_Globals[GM_GAM_PATH], 'gamcache') - GC_Defaults[GC_DRIVE_DIR] = GM_Globals[GM_GAM_PATH] - GC_Defaults[GC_SITE_DIR] = GM_Globals[GM_GAM_PATH] - - _getOldEnvVar(GC_CONFIG_DIR, 'GAMUSERCONFIGDIR') - _getOldEnvVar(GC_SITE_DIR, 'GAMSITECONFIGDIR') - _getOldEnvVar(GC_CACHE_DIR, 'GAMCACHEDIR') - _getOldEnvVar(GC_DRIVE_DIR, 'GAMDRIVEDIR') - _getOldEnvVar(GC_OAUTH2_TXT, 'OAUTHFILE') - _getOldEnvVar(GC_OAUTH2SERVICE_JSON, 'OAUTHSERVICEFILE') - if GC_Defaults[GC_OAUTH2SERVICE_JSON].find('.') == -1: - GC_Defaults[GC_OAUTH2SERVICE_JSON] += '.json' - _getOldEnvVar(GC_CLIENT_SECRETS_JSON, 'CLIENTSECRETS') - _getOldEnvVar(GC_DOMAIN, 'GA_DOMAIN') - _getOldEnvVar(GC_CUSTOMER_ID, 'CUSTOMER_ID') - _getOldEnvVar(GC_CHARSET, 'GAM_CHARSET') - _getOldEnvVar(GC_NUM_THREADS, 'GAM_THREADS') - _getOldEnvVar(GC_AUTO_BATCH_MIN, 'GAM_AUTOBATCH') - _getOldEnvVar(GC_BATCH_SIZE, 'GAM_BATCH_SIZE') - _getOldEnvVar(GC_CSV_HEADER_FILTER, 'GAM_CSV_HEADER_FILTER') - _getOldEnvVar(GC_CSV_HEADER_DROP_FILTER, 'GAM_CSV_HEADER_DROP_FILTER') - _getOldEnvVar(GC_CSV_ROW_FILTER, 'GAM_CSV_ROW_FILTER') - _getOldEnvVar(GC_TLS_MIN_VERSION, 'GAM_TLS_MIN_VERSION') - _getOldEnvVar(GC_TLS_MAX_VERSION, 'GAM_TLS_MAX_VERSION') - _getOldEnvVar(GC_CA_FILE, 'GAM_CA_FILE') - _getOldSignalFile(GC_DEBUG_LEVEL, 'debug.gam', filePresentValue=4, fileAbsentValue=0) - _getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt') - _getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt') -# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt') -# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True) - _getOldSignalFile(GC_NO_CACHE, 'allcache.txt', filePresentValue=False, fileAbsentValue=True) - _getOldSignalFile(GC_NO_UPDATE_CHECK, 'noupdatecheck.txt') -# Assign directories first - for itemName in GC_VAR_INFO: - if GC_VAR_INFO[itemName][GC_VAR_TYPE] == GC_TYPE_DIRECTORY: - GC_Values[itemName] = _getCfgDirectory(itemName) - for itemName in GC_VAR_INFO: - varType = GC_VAR_INFO[itemName][GC_VAR_TYPE] - if varType == GC_TYPE_FILE: - GC_Values[itemName] = _getCfgFile(itemName) - elif varType == GC_TYPE_HEADERFILTER: - GC_Values[itemName] = _getCfgHeaderFilter(itemName) - elif varType == GC_TYPE_ROWFILTER: - GC_Values[itemName] = _getCfgRowFilter(itemName) - else: - GC_Values[itemName] = GC_Defaults[itemName] - GM_Globals[GM_LAST_UPDATE_CHECK_TXT] = os.path.join(GC_Values[GC_CONFIG_DIR], FN_LAST_UPDATE_CHECK_TXT) - if not GC_Values[GC_NO_UPDATE_CHECK]: - doGAMCheckForUpdates() -# Globals derived from config file values - GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = None - GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = None - GM_Globals[GM_EXTRA_ARGS_DICT] = {'prettyPrint': GC_Values[GC_DEBUG_LEVEL] > 0} -# override httplib2 settings - httplib2.debuglevel = GC_Values[GC_DEBUG_LEVEL] - if os.path.isfile(os.path.join(GC_Values[GC_CONFIG_DIR], FN_EXTRA_ARGS_TXT)): - ea_config = configparser.ConfigParser() - ea_config.optionxform = str - ea_config.read(os.path.join(GC_Values[GC_CONFIG_DIR], FN_EXTRA_ARGS_TXT)) - GM_Globals[GM_EXTRA_ARGS_DICT].update(dict(ea_config.items('extra-args'))) - if GC_Values[GC_NO_CACHE]: - GM_Globals[GM_CACHE_DIR] = None - GM_Globals[GM_CACHE_DISCOVERY_ONLY] = False - else: - GM_Globals[GM_CACHE_DIR] = GC_Values[GC_CACHE_DIR] -# GM_Globals[GM_CACHE_DISCOVERY_ONLY] = GC_Values[GC_CACHE_DISCOVERY_ONLY] - GM_Globals[GM_CACHE_DISCOVERY_ONLY] = False - return True - -TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60), ('second', 1)] - -def getLocalGoogleTimeOffset(testLocation='www.googleapis.com'): - localUTC = datetime.datetime.now(datetime.timezone.utc) - try: - # we disable SSL verify so we can still get time even if clock - # is way off. This could be spoofed / MitM but we'll fail for those - # situations everywhere else but here. - badhttp = transport.create_http() - badhttp.disable_ssl_certificate_validation = True - googleUTC = dateutil.parser.parse(badhttp.request('https://'+testLocation, 'HEAD')[0]['date']) - except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e: - controlflow.system_error_exit(4, str(e)) - offset = remainder = int(abs((localUTC-googleUTC).total_seconds())) - timeoff = [] - for tou in TIME_OFFSET_UNITS: - uval, remainder = divmod(remainder, tou[1]) - if uval: - timeoff.append(f'{uval} {tou[0]}{"s" if uval != 1 else ""}') - if not timeoff: - timeoff.append('less than 1 second') - nicetime = ', '.join(timeoff) - return (offset, nicetime) - -def doGAMCheckForUpdates(forceCheck=False): - - def _gamLatestVersionNotAvailable(): - if forceCheck: - controlflow.system_error_exit(4, 'GAM Latest Version information not available') - - current_version = gam_version - now_time = int(time.time()) - if forceCheck: - check_url = GAM_ALL_RELEASES # includes pre-releases - else: - last_check_time_str = fileutils.read_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], continue_on_error=True, display_errors=False) - last_check_time = int(last_check_time_str) if last_check_time_str and last_check_time_str.isdigit() else 0 - if last_check_time > now_time-604800: - return - check_url = GAM_LATEST_RELEASE # latest full release - headers = {'Accept': 'application/vnd.github.v3.text+json'} - simplehttp = transport.create_http(timeout=10) - try: - (_, c) = simplehttp.request(check_url, 'GET', headers=headers) - try: - release_data = json.loads(c.decode(UTF8)) - except ValueError: - _gamLatestVersionNotAvailable() - return - if isinstance(release_data, list): - release_data = release_data[0] # only care about latest release - if not isinstance(release_data, dict) or 'tag_name' not in release_data: - _gamLatestVersionNotAvailable() - return - latest_version = release_data['tag_name'] - if latest_version[0].lower() == 'v': - latest_version = latest_version[1:] - if forceCheck or (latest_version > current_version): - print(f'Version Check:\n Current: {current_version}\n Latest: {latest_version}') - if latest_version <= current_version: - fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], str(now_time), continue_on_error=True, display_errors=forceCheck) - return - announcement = release_data.get('body_text', 'No details about this release') - sys.stderr.write(f'\nGAM {latest_version} release notes:\n\n') - sys.stderr.write(announcement) - try: - printLine(MESSAGE_HIT_CONTROL_C_TO_UPDATE) - time.sleep(15) - except KeyboardInterrupt: - webbrowser.open(release_data['html_url']) - printLine(MESSAGE_GAM_EXITING_FOR_UPDATE) - sys.exit(0) - fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], str(now_time), continue_on_error=True, display_errors=forceCheck) - return - except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError, RuntimeError, socket.timeout): - return - -def getOSPlatform(): - myos = platform.system() - if myos == 'Linux': - pltfrm = ' '.join(distro.linux_distribution(full_distribution_name=False)).title() - elif myos == 'Windows': - pltfrm = ' '.join(platform.win32_ver()) - elif myos == 'Darwin': - myos = 'MacOS' - mac_ver = platform.mac_ver()[0] - minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14 - codename = MACOS_CODENAMES.get(minor_ver, '') - pltfrm = ' '.join([codename, mac_ver]) - else: - pltfrm = platform.platform() - return f'{myos} {pltfrm}' - -def doGAMVersion(checkForArgs=True): - force_check = extended = simple = timeOffset = False - testLocation = 'www.googleapis.com' - if checkForArgs: - i = 2 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'check': - force_check = True - i += 1 - elif myarg == 'simple': - simple = True - i += 1 - elif myarg == 'extended': - extended = True - timeOffset = True - i += 1 - elif myarg == 'timeoffset': - timeOffset = True - i += 1 - elif myarg == 'location': - testLocation = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam version") - if simple: - sys.stdout.write(gam_version) - return - pyversion = platform.python_version() - cpu_bits = struct.calcsize('P') * 8 - api_client_ver = pkg_resources.get_distribution("google-api-python-client").version - print((f'GAM {gam_version} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n' - f'{gam_author}\n' - f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n' - f'google-api-python-client {api_client_ver}\n' - f'{getOSPlatform()} {platform.machine()}\n' - f'Path: {GM_Globals[GM_GAM_PATH]}')) - if sys.platform.startswith('win') and \ - cpu_bits == 32 and \ - platform.machine().find('64') != -1: - print(MESSAGE_UPDATE_GAM_TO_64BIT) - if timeOffset: - offset, nicetime = getLocalGoogleTimeOffset(testLocation) - print(MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY % (testLocation, nicetime)) - if offset > MAX_LOCAL_GOOGLE_TIME_OFFSET: - controlflow.system_error_exit(4, 'Please fix your system time.') - if force_check: - doGAMCheckForUpdates(forceCheck=True) - if extended: - print(ssl.OPENSSL_VERSION) - tls_ver, cipher_name, used_ip = _getServerTLSUsed(testLocation) - print(f'{testLocation} ({used_ip}) connects using {tls_ver} {cipher_name}') - -def _getServerTLSUsed(location): - url = f'https://{location}' - _, netloc, _, _, _, _ = urlparse(url) - conn = f'https:{netloc}' - httpc = transport.create_http() - headers = {'user-agent': GAM_INFO} - retries = 5 - for n in range(1, retries+1): - try: - httpc.request(url, headers=headers) - break - except (httplib2.ServerNotFoundError, RuntimeError) as e: - if n != retries: - httpc.connections = {} - controlflow.wait_on_failure(n, retries, str(e)) - continue - controlflow.system_error_exit(4, str(e)) - cipher_name, tls_ver, _ = httpc.connections[conn].sock.cipher() - used_ip = httpc.connections[conn].sock.getpeername()[0] - return tls_ver, cipher_name, used_ip - -def _getSvcAcctData(): - if not GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]: - json_string = fileutils.read_file(GC_Values[GC_OAUTH2SERVICE_JSON], continue_on_error=True, display_errors=True) - if not json_string: - printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON) - controlflow.system_error_exit(6, None) - GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string) - -def getSvcAcctCredentials(scopes, act_as): - try: - _getSvcAcctData() - credentials = google.oauth2.service_account.Credentials.from_service_account_info(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]) - credentials = credentials.with_scopes(scopes) - if act_as: - credentials = credentials.with_subject(act_as) - GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_id'] - return credentials - except (ValueError, KeyError): - printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON) - controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON]) - -def getAPIVersion(api): - version = API_VER_MAPPING.get(api, 'v1') - if api in ['directory', 'reports', 'datatransfer']: - api = 'admin' - elif api == 'drive3': - api = 'drive' - elif api == 'cloudresourcemanagerv1': - api = 'cloudresourcemanager' - return (api, version, f'{api}-{version}') - -def readDiscoveryFile(api_version): - disc_filename = f'{api_version}.json' - disc_file = os.path.join(GM_Globals[GM_GAM_PATH], disc_filename) - if hasattr(sys, '_MEIPASS'): - pyinstaller_disc_file = os.path.join(sys._MEIPASS, disc_filename) - else: - pyinstaller_disc_file = None - if os.path.isfile(disc_file): - json_string = fileutils.read_file(disc_file) - elif pyinstaller_disc_file: - json_string = fileutils.read_file(pyinstaller_disc_file) - else: - controlflow.system_error_exit(11, MESSAGE_NO_DISCOVERY_INFORMATION.format(disc_file)) - try: - discovery = json.loads(json_string) - return (disc_file, discovery) - except ValueError: - controlflow.invalid_json_exit(disc_file) - -def getOauth2TxtStorageCredentials(): - try: - return auth.get_admin_credentials() - except auth.oauth.InvalidCredentialsFileError: - # Maintain legacy behavior of this method that returns None if no - # credential file is present. - return None - -def getValidOauth2TxtCredentials(force_refresh=False): - """Gets OAuth2 credentials which are guaranteed to be fresh and valid.""" - try: - credentials = auth.get_admin_credentials() - except auth.oauth.InvalidCredentialsFileError: - doRequestOAuth() # Make a new request which should store new creds. - return getValidOauth2TxtCredentials(force_refresh=force_refresh) - - if credentials.expired or force_refresh: - request = transport.create_request() - credentials.refresh(request) - return credentials - -def getService(api, http): - api, version, api_version = getAPIVersion(api) - if api in GM_Globals[GM_CURRENT_API_SERVICES] and version in GM_Globals[GM_CURRENT_API_SERVICES][api]: - service = googleapiclient.discovery.build_from_document(GM_Globals[GM_CURRENT_API_SERVICES][api][version], http=http) - if GM_Globals[GM_CACHE_DISCOVERY_ONLY]: - http.cache = None - return service - if api in V1_DISCOVERY_APIS: - discoveryServiceUrl = googleapiclient.discovery.DISCOVERY_URI - else: - discoveryServiceUrl = googleapiclient.discovery.V2_DISCOVERY_URI - retries = 3 - for n in range(1, retries+1): - try: - service = googleapiclient.discovery.build(api, version, http=http, cache_discovery=False, discoveryServiceUrl=discoveryServiceUrl) - GM_Globals[GM_CURRENT_API_SERVICES].setdefault(api, {}) - GM_Globals[GM_CURRENT_API_SERVICES][api][version] = service._rootDesc.copy() - if GM_Globals[GM_CACHE_DISCOVERY_ONLY]: - http.cache = None - return service - except (httplib2.ServerNotFoundError, RuntimeError) as e: - if n != retries: - http.connections = {} - controlflow.wait_on_failure(n, retries, str(e)) - continue - controlflow.system_error_exit(4, str(e)) - except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: - http.cache = None - if n != retries: - controlflow.wait_on_failure(n, retries, str(e)) - continue - controlflow.system_error_exit(17, str(e)) - except (http_client.ResponseNotReady, socket.error, - googleapiclient.errors.HttpError) as e: - if n != retries: - controlflow.wait_on_failure(n, retries, str(e)) - continue - controlflow.system_error_exit(3, str(e)) - except googleapiclient.errors.UnknownApiNameOrVersion: - break - disc_file, discovery = readDiscoveryFile(api_version) - try: - service = googleapiclient.discovery.build_from_document(discovery, http=http) - GM_Globals[GM_CURRENT_API_SERVICES].setdefault(api, {}) - GM_Globals[GM_CURRENT_API_SERVICES][api][version] = service._rootDesc.copy() - if GM_Globals[GM_CACHE_DISCOVERY_ONLY]: - http.cache = None - return service - except (KeyError, ValueError): - controlflow.invalid_json_exit(disc_file) - -def buildGAPIObject(api): - GM_Globals[GM_CURRENT_API_USER] = None - credentials = getValidOauth2TxtCredentials() - credentials.user_agent = GAM_INFO - http = transport.AuthorizedHttp(credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR])) - service = getService(api, http) - if GC_Values[GC_DOMAIN]: - if not GC_Values[GC_CUSTOMER_ID]: - resp, result = service._http.request(f'https://www.googleapis.com/admin/directory/v1/users?domain={GC_Values[GC_DOMAIN]}&maxResults=1&fields=users(customerId)') - try: - resultObj = json.loads(result) - except ValueError: - controlflow.system_error_exit(8, f'Unexpected response: {result}') - if resp['status'] in ['403', '404']: - try: - message = resultObj['error']['errors'][0]['message'] - except KeyError: - message = resultObj['error']['message'] - controlflow.system_error_exit(8, f'{message} - {GC_Values[GC_DOMAIN]}') - try: - GC_Values[GC_CUSTOMER_ID] = resultObj['users'][0]['customerId'] - except KeyError: - GC_Values[GC_CUSTOMER_ID] = MY_CUSTOMER - else: - GC_Values[GC_DOMAIN] = _getValueFromOAuth('hd', credentials=credentials) - if not GC_Values[GC_CUSTOMER_ID]: - GC_Values[GC_CUSTOMER_ID] = MY_CUSTOMER - return service - -# Convert UID to email address -def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, email_types=['user']): - if isinstance(email_types, str): - email_types = email_types.split(',') - normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID) - if normalizedEmailAddressOrUID.find('@') > 0: - return normalizedEmailAddressOrUID - if not cd: - cd = buildGAPIObject('directory') - if 'user' in email_types: - try: - result = gapi.call(cd.users(), 'get', - throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND], - userKey=normalizedEmailAddressOrUID, fields='primaryEmail') - if 'primaryEmail' in result: - return result['primaryEmail'].lower() - except gapi.errors.GapiUserNotFoundError: - pass - if 'group' in email_types: - try: - result = gapi.call(cd.groups(), 'get', - throw_reasons=[gapi.errors.ErrorReason.GROUP_NOT_FOUND], - groupKey=normalizedEmailAddressOrUID, fields='email') - if 'email' in result: - return result['email'].lower() - except gapi.errors.GapiGroupNotFoundError: - pass - if 'resource' in email_types: - try: - result = gapi.call(cd.resources().calendars(), 'get', - throw_reasons=[gapi.errors.ErrorReason.RESOURCE_NOT_FOUND], - calendarResourceId=normalizedEmailAddressOrUID, - customer=GC_Values[GC_CUSTOMER_ID], fields='resourceEmail') - if 'resourceEmail' in result: - return result['resourceEmail'].lower() - except gapi.errors.GapiResourceNotFoundError: - pass - return normalizedEmailAddressOrUID - -# Convert email address to UID -def convertEmailAddressToUID(emailAddressOrUID, cd=None, email_type='user'): - normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID) - if normalizedEmailAddressOrUID.find('@') > 0: - if not cd: - cd = buildGAPIObject('directory') - if email_type != 'group': - try: - result = gapi.call(cd.users(), 'get', - throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND], - userKey=normalizedEmailAddressOrUID, fields='id') - if 'id' in result: - return result['id'] - except gapi.errors.GapiUserNotFoundError: - pass - try: - result = gapi.call(cd.groups(), 'get', - throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND], - groupKey=normalizedEmailAddressOrUID, fields='id') - if 'id' in result: - return result['id'] - except gapi.errors.GapiNotFoundError: - pass - return None - return normalizedEmailAddressOrUID - -def buildGAPIServiceObject(api, act_as, showAuthError=True): - http = transport.create_http(cache=GM_Globals[GM_CACHE_DIR]) - service = getService(api, http) - GM_Globals[GM_CURRENT_API_USER] = act_as - GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get(api, service._rootDesc['auth']['oauth2']['scopes']) - credentials = getSvcAcctCredentials(GM_Globals[GM_CURRENT_API_SCOPES], act_as) - request = transport.create_request(http) - retries = 3 - for n in range(1, retries+1): - try: - credentials.refresh(request) - service._http = transport.AuthorizedHttp(credentials, http=http) - break - except (httplib2.ServerNotFoundError, RuntimeError) as e: - if n != retries: - http.connections = {} - controlflow.wait_on_failure(n, retries, str(e)) - continue - controlflow.system_error_exit(4, e) - except google.auth.exceptions.RefreshError as e: - if isinstance(e.args, tuple): - e = e.args[0] - if showAuthError: - display.print_error(f'User {GM_Globals[GM_CURRENT_API_USER]}: {str(e)}') - return gapi.handle_oauth_token_error(str(e), True) - return service - -def buildAlertCenterGAPIObject(user): - userEmail = convertUIDtoEmailAddress(user) - return (userEmail, buildGAPIServiceObject('alertcenter', userEmail)) - -def buildActivityGAPIObject(user): - userEmail = convertUIDtoEmailAddress(user) - return (userEmail, buildGAPIServiceObject('appsactivity', userEmail)) - -def buildDriveGAPIObject(user): - userEmail = convertUIDtoEmailAddress(user) - return (userEmail, buildGAPIServiceObject('drive', userEmail)) - -def buildDrive3GAPIObject(user): - userEmail = convertUIDtoEmailAddress(user) - return (userEmail, buildGAPIServiceObject('drive3', userEmail)) - -def buildGmailGAPIObject(user): - userEmail = convertUIDtoEmailAddress(user) - return (userEmail, buildGAPIServiceObject('gmail', userEmail)) - -def printPassFail(description, result): - print(f' {description:74} {result}') - -def doCheckServiceAccount(users): - i = 5 - test_pass = createGreenText('PASS') - test_fail = createRedText('FAIL') - test_warn = createYellowText('WARN') - check_scopes = [] - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['scope', 'scopes']: - check_scopes = sys.argv[i+1].replace(',', ' ').split() - i += 2 - else: - controlflow.invalid_argument_exit(myarg, "gam user check serviceaccount") - print('Computer clock status:') - timeOffset, nicetime = getLocalGoogleTimeOffset() - if timeOffset < MAX_LOCAL_GOOGLE_TIME_OFFSET: - time_status = test_pass - else: - time_status = test_fail - printPassFail(MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY % ('www.googleapis.com', nicetime), time_status) - oa2 = getService('oauth2', transport.create_http()) - print('Service Account Private Key Authentication:') - # We are explicitly not doing DwD here, just confirming service account can auth - auth_error = '' - try: - credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE], None) - request = transport.create_request() - credentials.refresh(request) - sa_token_info = gapi.call(oa2, 'tokeninfo', access_token=credentials.token) - if sa_token_info: - sa_token_result = test_pass - else: - sa_token_result = test_fail - except google.auth.exceptions.RefreshError as e: - sa_token_result = test_fail - auth_error = str(e.args[0]) - printPassFail(f'Authenticating...{auth_error}', sa_token_result) - if sa_token_result == test_fail: - controlflow.system_error_exit(3, 'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"') - print('Checking key age. Google recommends rotating keys on a routine basis...') - try: - iam = buildGAPIServiceObject('iam', None) - project = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] - key_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] - name = f'projects/-/serviceAccounts/{project}/keys/{key_id}' - key = gapi.call(iam.projects().serviceAccounts().keys(), 'get', name=name, throw_reasons=[gapi.errors.ErrorReason.FOUR_O_THREE]) - # Both Google and GAM set key valid after to day before creation - key_created = dateutil.parser.parse(key['validAfterTime'], ignoretz=True) + datetime.timedelta(days=1) - key_age = datetime.datetime.now() - key_created - key_days = key_age.days - if key_days > 30: - print('Your key is old. Recommend running "gam rotate sakey" to get a new key') - key_age_result = test_warn - else: - key_age_result = test_pass - except googleapiclient.errors.HttpError: - key_age_result = test_warn - key_days = 'UNKNOWN' - print('Unable to check key age, please run "gam update project"') - printPassFail(f'Key is {key_days} days old', key_age_result) - if not check_scopes: - for _, scopes in list(API_SCOPE_MAPPING.items()): - for scope in scopes: - if scope not in check_scopes: - check_scopes.append(scope) - check_scopes.sort() - for user in users: - user = user.lower() - all_scopes_pass = True - #oa2 = getService('oauth2', transport.create_http()) - print(f'Domain-Wide Delegation authentication as {user}:') - for scope in check_scopes: - # try with and without email scope - for scopes in [[scope, USERINFO_EMAIL_SCOPE], [scope]]: - try: - credentials = getSvcAcctCredentials(scopes, user) - credentials.refresh(request) - break - except (httplib2.ServerNotFoundError, RuntimeError) as e: - controlflow.system_error_exit(4, e) - except google.auth.exceptions.RefreshError: - continue - if credentials.token: - token_info = gapi.call(oa2, 'tokeninfo', access_token=credentials.token) - if scope in token_info.get('scope', '').split(' ') and \ - user == token_info.get('email', user).lower(): - result = test_pass - else: - result = test_fail - all_scopes_pass = False - else: - result = test_fail - all_scopes_pass = False - printPassFail(scope, result) - service_account = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] - if all_scopes_pass: - print(f'\nAll scopes passed!\nService account {service_account} is fully authorized.') - continue - user_domain = user[user.find('@')+1:] - # Tack on email scope for more accurate checking - check_scopes.append(USERINFO_EMAIL_SCOPE) - long_url = (f'https://admin.google.com/{user_domain}/ManageOauthClients' - f'?clientScopeToAdd={",".join(check_scopes)}' - f'&clientNameToAdd={service_account}') - short_url = utils.shorten_url(long_url) - scopes_failed = f'''Some scopes failed! To authorize them, please go to: - - {short_url} - -You will be redirected to the G Suite admin console. The Client Name and API -Scopes fields will be pre-populated. Please click Authorize to allow these -scopes access. After authorizing it may take some time for this test to pass so -go grab a cup of coffee and then try this command again. -''' - controlflow.system_error_exit(1, scopes_failed) - -# Batch processing request_id fields -RI_ENTITY = 0 -RI_J = 1 -RI_JCOUNT = 2 -RI_ITEM = 3 -RI_ROLE = 4 - -def batchRequestID(entityName, j, jcount, item, role=''): - return f'{entityName}\n{j}\n{jcount}\n{item}\n{role}' - -def watchGmail(users): - project = f'projects/{_getCurrentProjectID()}' - gamTopics = project+'/topics/gam-pubsub-gmail-' - gamSubscriptions = project+'/subscriptions/gam-pubsub-gmail-' - pubsub = buildGAPIObject('pubsub') - topics = gapi.get_all_pages(pubsub.projects().topics(), 'list', items='topics', project=project) - for atopic in topics: - if atopic['name'].startswith(gamTopics): - topic = atopic['name'] - break - else: - topic = gamTopics+str(uuid.uuid4()) - gapi.call(pubsub.projects().topics(), 'create', name=topic) - body = {'policy': {'bindings': [{'members': ['serviceAccount:gmail-api-push@system.gserviceaccount.com'], 'role': 'roles/pubsub.editor'}]}} - gapi.call(pubsub.projects().topics(), 'setIamPolicy', resource=topic, body=body) - subscriptions = gapi.get_all_pages(pubsub.projects().topics().subscriptions(), 'list', items='subscriptions', topic=topic) - for asubscription in subscriptions: - if asubscription.startswith(gamSubscriptions): - subscription = asubscription - break - else: - subscription = gamSubscriptions+str(uuid.uuid4()) - gapi.call(pubsub.projects().subscriptions(), 'create', name=subscription, body={'topic': topic}) - gmails = {} - for user in users: - gmails[user] = {'g': buildGmailGAPIObject(user)[1]} - gapi.call(gmails[user]['g'].users(), 'watch', userId='me', body={'topicName': topic}) - gmails[user]['seen_historyId'] = gapi.call(gmails[user]['g'].users(), 'getProfile', userId='me', fields='historyId')['historyId'] - print('Watching for events...') - while True: - results = gapi.call(pubsub.projects().subscriptions(), 'pull', subscription=subscription, body={'maxMessages': 100}) - if 'receivedMessages' in results: - ackIds = [] - update_history = [] - for message in results['receivedMessages']: - if 'data' in message['message']: - decoded_message = json.loads(base64.b64decode(message['message']['data'])) - if 'historyId' in decoded_message: - update_history.append(decoded_message['emailAddress']) - if 'ackId' in message: - ackIds.append(message['ackId']) - if ackIds: - gapi.call(pubsub.projects().subscriptions(), 'acknowledge', subscription=subscription, body={'ackIds': ackIds}) - if update_history: - for a_user in update_history: - results = gapi.call(gmails[a_user]['g'].users().history(), 'list', userId='me', startHistoryId=gmails[a_user]['seen_historyId']) - if 'history' in results: - for history in results['history']: - if list(history) == ['messages', 'id']: - continue - if 'labelsAdded' in history: - for labelling in history['labelsAdded']: - print(f'{a_user} labels {", ".join(labelling["labelIds"])} added to {labelling["message"]["id"]}') - if 'labelsRemoved' in history: - for labelling in history['labelsRemoved']: - print(f'{a_user} labels {", ".join(labelling["labelIds"])} removed from {labelling["message"]["id"]}') - if 'messagesDeleted' in history: - for deleting in history['messagesDeleted']: - print(f'{a_user} permanently deleted message {deleting["message"]["id"]}') - if 'messagesAdded' in history: - for adding in history['messagesAdded']: - print(f'{a_user} created message {adding["message"]["id"]} with labels {", ".join(adding["message"]["labelIds"])}') - gmails[a_user]['seen_historyId'] = results['historyId'] - -def addDelegates(users, i): - if i == 4: - if sys.argv[i].lower() != 'to': - controlflow.missing_argument_exit("to", "gam delegate") - i += 1 - delegate = normalizeEmailAddressOrUID(sys.argv[i], noUid=True) - i = 0 - count = len(users) - for delegator in users: - i += 1 - delegator, gmail = buildGmailGAPIObject(delegator) - if not gmail: - continue - print(f'Giving {delegate} delegate access to {delegator}{currentCount(i, count)}') - gapi.call(gmail.users().settings().delegates(), 'create', soft_errors=True, userId='me', body={'delegateEmail': delegate}) - -def gen_sha512_hash(password): - if platform.system() == 'Windows': - return sha512_crypt.hash(password, rounds=5000) - return crypt(password) - -def printShowDelegates(users, csvFormat): - if csvFormat: - todrive = False - csvRows = [] - titles = ['User', 'delegateAddress', 'delegationStatus'] - else: - csvStyle = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if not csvFormat and myarg == 'csv': - csvStyle = True - i += 1 - elif csvFormat and myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show delegates") - count = len(users) - i = 1 - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - sys.stderr.write(f'Getting delegates for {user}{currentCountNL(i, count)}') - i += 1 - delegates = gapi.call(gmail.users().settings().delegates(), 'list', soft_errors=True, userId='me') - if delegates and 'delegates' in delegates: - for delegate in delegates['delegates']: - delegateAddress = delegate['delegateEmail'] - status = delegate['verificationStatus'] - if csvFormat: - row = {'User': user, 'delegateAddress': delegateAddress, 'delegationStatus': status} - csvRows.append(row) - else: - if csvStyle: - print(f'{user},{delegateAddress},{status}') - else: - print(f'Delegator: {user}\n Status: {status}\n Delegate Email: {delegateAddress}\n') - if not csvFormat and not csvStyle and delegates['delegates']: - print(f'Total {len(delegates["delegates"])}') - if csvFormat: - display.write_csv_file(csvRows, titles, 'Delegates', todrive) - -def deleteDelegate(users): - delegate = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Deleting {delegate} delegate access to {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings().delegates(), 'delete', soft_errors=True, userId='me', delegateEmail=delegate) - -def doAddCourseParticipant(): - croom = buildGAPIObject('classroom') - courseId = addCourseIdScope(sys.argv[2]) - noScopeCourseId = removeCourseIdScope(courseId) - participant_type = sys.argv[4].lower() - new_id = sys.argv[5] - if participant_type in ['student', 'students']: - new_id = normalizeEmailAddressOrUID(new_id) - gapi.call(croom.courses().students(), 'create', courseId=courseId, body={'userId': new_id}) - print(f'Added {new_id} as a student of course {noScopeCourseId}') - elif participant_type in ['teacher', 'teachers']: - new_id = normalizeEmailAddressOrUID(new_id) - gapi.call(croom.courses().teachers(), 'create', courseId=courseId, body={'userId': new_id}) - print(f'Added {new_id} as a teacher of course {noScopeCourseId}') - elif participant_type in ['alias']: - new_id = addCourseIdScope(new_id) - gapi.call(croom.courses().aliases(), 'create', courseId=courseId, body={'alias': new_id}) - print(f'Added {removeCourseIdScope(new_id)} as an alias of course {noScopeCourseId}') - else: - controlflow.invalid_argument_exit(participant_type, "gam course ID add") - -def doSyncCourseParticipants(): - courseId = addCourseIdScope(sys.argv[2]) - participant_type = sys.argv[4].lower() - diff_entity_type = sys.argv[5].lower() - diff_entity = sys.argv[6] - current_course_users = getUsersToModify(entity_type=participant_type, entity=courseId) - print() - current_course_users = [x.lower() for x in current_course_users] - if diff_entity_type == 'courseparticipants': - diff_entity_type = participant_type - diff_against_users = getUsersToModify(entity_type=diff_entity_type, entity=diff_entity) - print() - diff_against_users = [x.lower() for x in diff_against_users] - to_add = list(set(diff_against_users) - set(current_course_users)) - to_remove = list(set(current_course_users) - set(diff_against_users)) - gam_commands = [] - for add_email in to_add: - gam_commands.append(['gam', 'course', courseId, 'add', participant_type, add_email]) - for remove_email in to_remove: - gam_commands.append(['gam', 'course', courseId, 'remove', participant_type, remove_email]) - run_batch(gam_commands) - -def doDelCourseParticipant(): - croom = buildGAPIObject('classroom') - courseId = addCourseIdScope(sys.argv[2]) - noScopeCourseId = removeCourseIdScope(courseId) - participant_type = sys.argv[4].lower() - remove_id = sys.argv[5] - if participant_type in ['student', 'students']: - remove_id = normalizeEmailAddressOrUID(remove_id) - gapi.call(croom.courses().students(), 'delete', courseId=courseId, userId=remove_id) - print(f'Removed {remove_id} as a student of course {noScopeCourseId}') - elif participant_type in ['teacher', 'teachers']: - remove_id = normalizeEmailAddressOrUID(remove_id) - gapi.call(croom.courses().teachers(), 'delete', courseId=courseId, userId=remove_id) - print(f'Removed {remove_id} as a teacher of course {noScopeCourseId}') - elif participant_type in ['alias']: - remove_id = addCourseIdScope(remove_id) - gapi.call(croom.courses().aliases(), 'delete', courseId=courseId, alias=remove_id) - print(f'Removed {removeCourseIdScope(remove_id)} as an alias of course {noScopeCourseId}') - else: - controlflow.invalid_argument_exit(participant_type, "gam course ID delete") - -def doDelCourse(): - croom = buildGAPIObject('classroom') - courseId = addCourseIdScope(sys.argv[3]) - gapi.call(croom.courses(), 'delete', id=courseId) - print(f'Deleted Course {courseId}') - -def _getValidatedState(state, validStates): - state = state.upper() - if state not in validStates: - controlflow.expected_argument_exit("course state", ", ".join(validStates).lower(), state.lower()) - return state - -def getCourseAttribute(myarg, value, body, croom, function): - if myarg == 'name': - body['name'] = value - elif myarg == 'section': - body['section'] = value - elif myarg == 'heading': - body['descriptionHeading'] = value - elif myarg == 'description': - body['description'] = value.replace('\\n', '\n') - elif myarg == 'room': - body['room'] = value - elif myarg in ['owner', 'ownerid', 'teacher']: - body['ownerId'] = normalizeEmailAddressOrUID(value) - elif myarg in ['state', 'status']: - validStates = gapi.get_enum_values_minus_unspecified(croom._rootDesc['schemas']['Course']['properties']['courseState']['enum']) - body['courseState'] = _getValidatedState(value, validStates) - else: - controlflow.invalid_argument_exit(myarg, f"gam {function} course") - -def _getCourseStates(croom, value, courseStates): - validStates = gapi.get_enum_values_minus_unspecified(croom._rootDesc['schemas']['Course']['properties']['courseState']['enum']) - for state in value.replace(',', ' ').split(): - courseStates.append(_getValidatedState(state, validStates)) - -def doUpdateCourse(): - croom = buildGAPIObject('classroom') - courseId = addCourseIdScope(sys.argv[3]) - body = {} - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - getCourseAttribute(myarg, sys.argv[i+1], body, croom, 'update') - i += 2 - updateMask = ','.join(list(body)) - body['id'] = courseId - result = gapi.call(croom.courses(), 'patch', id=courseId, body=body, updateMask=updateMask) - print(f'Updated Course {result["id"]}') - -def doCreateDomain(): - cd = buildGAPIObject('directory') - domain_name = sys.argv[3] - body = {'domainName': domain_name} - gapi.call(cd.domains(), 'insert', customer=GC_Values[GC_CUSTOMER_ID], body=body) - print(f'Added domain {domain_name}') - -def doCreateDomainAlias(): - cd = buildGAPIObject('directory') - body = {} - body['domainAliasName'] = sys.argv[3] - body['parentDomainName'] = sys.argv[4] - gapi.call(cd.domainAliases(), 'insert', customer=GC_Values[GC_CUSTOMER_ID], body=body) - -def doUpdateDomain(): - cd = buildGAPIObject('directory') - domain_name = sys.argv[3] - i = 4 - body = {} - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'primary': - body['customerDomain'] = domain_name - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam update domain") - gapi.call(cd.customers(), 'update', customerKey=GC_Values[GC_CUSTOMER_ID], body=body) - print(f'{domain_name} is now the primary domain.') - -def doGetDomainInfo(): - if (len(sys.argv) < 4) or (sys.argv[3] == 'logo'): - gapi.directory.customer.doGetCustomerInfo() - return - cd = buildGAPIObject('directory') - domainName = sys.argv[3] - result = gapi.call(cd.domains(), 'get', customer=GC_Values[GC_CUSTOMER_ID], domainName=domainName) - if 'creationTime' in result: - result['creationTime'] = utils.formatTimestampYMDHMSF(result['creationTime']) - if 'domainAliases' in result: - for i in range(0, len(result['domainAliases'])): - if 'creationTime' in result['domainAliases'][i]: - result['domainAliases'][i]['creationTime'] = utils.formatTimestampYMDHMSF(result['domainAliases'][i]['creationTime']) - display.print_json(result) - -def doGetDomainAliasInfo(): - cd = buildGAPIObject('directory') - alias = sys.argv[3] - result = gapi.call(cd.domainAliases(), 'get', customer=GC_Values[GC_CUSTOMER_ID], domainAliasName=alias) - if 'creationTime' in result: - result['creationTime'] = utils.formatTimestampYMDHMSF(result['creationTime']) - display.print_json(result) - -def doDelDomain(): - cd = buildGAPIObject('directory') - domainName = sys.argv[3] - gapi.call(cd.domains(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], domainName=domainName) - -def doDelDomainAlias(): - cd = buildGAPIObject('directory') - domainAliasName = sys.argv[3] - gapi.call(cd.domainAliases(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], domainAliasName=domainAliasName) - -def doPrintDomains(): - cd = buildGAPIObject('directory') - todrive = False - titles = ['domainName',] - csvRows = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print domains") - results = gapi.call(cd.domains(), 'list', customer=GC_Values[GC_CUSTOMER_ID]) - for domain in results['domains']: - domain_attributes = {} - domain['type'] = ['secondary', 'primary'][domain['isPrimary']] - for attr in domain: - if attr in ['kind', 'etag', 'domainAliases', 'isPrimary']: - continue - if attr in ['creationTime',]: - domain[attr] = utils.formatTimestampYMDHMSF(domain[attr]) - if attr not in titles: - titles.append(attr) - domain_attributes[attr] = domain[attr] - csvRows.append(domain_attributes) - if 'domainAliases' in domain: - for aliasdomain in domain['domainAliases']: - aliasdomain['domainName'] = aliasdomain['domainAliasName'] - del aliasdomain['domainAliasName'] - aliasdomain['type'] = 'alias' - aliasdomain_attributes = {} - for attr in aliasdomain: - if attr in ['kind', 'etag']: - continue - if attr in ['creationTime',]: - aliasdomain[attr] = utils.formatTimestampYMDHMSF(aliasdomain[attr]) - if attr not in titles: - titles.append(attr) - aliasdomain_attributes[attr] = aliasdomain[attr] - csvRows.append(aliasdomain_attributes) - display.write_csv_file(csvRows, titles, 'Domains', todrive) - -def doPrintDomainAliases(): - cd = buildGAPIObject('directory') - todrive = False - titles = ['domainAliasName',] - csvRows = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print domainaliases") - results = gapi.call(cd.domainAliases(), 'list', customer=GC_Values[GC_CUSTOMER_ID]) - for domainAlias in results['domainAliases']: - domainAlias_attributes = {} - for attr in domainAlias: - if attr in ['kind', 'etag']: - continue - if attr == 'creationTime': - domainAlias[attr] = utils.formatTimestampYMDHMSF(domainAlias[attr]) - if attr not in titles: - titles.append(attr) - domainAlias_attributes[attr] = domainAlias[attr] - csvRows.append(domainAlias_attributes) - display.write_csv_file(csvRows, titles, 'Domains', todrive) - -def doDelAdmin(): - cd = buildGAPIObject('directory') - roleAssignmentId = sys.argv[3] - print(f'Deleting Admin Role Assignment {roleAssignmentId}') - gapi.call(cd.roleAssignments(), 'delete', - customer=GC_Values[GC_CUSTOMER_ID], roleAssignmentId=roleAssignmentId) - -def doCreateAdmin(): - cd = buildGAPIObject('directory') - user = normalizeEmailAddressOrUID(sys.argv[3]) - body = {'assignedTo': convertEmailAddressToUID(user, cd)} - role = sys.argv[4] - body['roleId'] = getRoleId(role) - body['scopeType'] = sys.argv[5].upper() - if body['scopeType'] not in ['CUSTOMER', 'ORG_UNIT']: - controlflow.expected_argument_exit("scope type", ", ".join(["customer", "org_unit"]), body["scopeType"]) - if body['scopeType'] == 'ORG_UNIT': - orgUnit, orgUnitId = getOrgUnitId(sys.argv[6], cd) - body['orgUnitId'] = orgUnitId[3:] - scope = f'ORG_UNIT {orgUnit}' - else: - scope = 'CUSTOMER' - print(f'Giving {user} admin role {role} for {scope}') - gapi.call(cd.roleAssignments(), 'insert', - customer=GC_Values[GC_CUSTOMER_ID], body=body) - -def doPrintAdminRoles(): - cd = buildGAPIObject('directory') - todrive = False - titles = ['roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', 'isSystemRole'] - fields = f'nextPageToken,items({",".join(titles)})' - csvRows = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print adminroles") - roles = gapi.get_all_pages(cd.roles(), 'list', 'items', - customer=GC_Values[GC_CUSTOMER_ID], fields=fields) - for role in roles: - role_attrib = {} - for key, value in list(role.items()): - role_attrib[key] = value - csvRows.append(role_attrib) - display.write_csv_file(csvRows, titles, 'Admin Roles', todrive) - -def doPrintAdmins(): - cd = buildGAPIObject('directory') - roleId = None - userKey = None - todrive = False - fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)' - titles = ['roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser', 'scopeType', 'orgUnitId', 'orgUnit'] - csvRows = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'user': - userKey = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg == 'role': - roleId = getRoleId(sys.argv[i+1]) - i += 2 - elif myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print admins") - admins = gapi.get_all_pages(cd.roleAssignments(), 'list', 'items', - customer=GC_Values[GC_CUSTOMER_ID], userKey=userKey, roleId=roleId, fields=fields) - for admin in admins: - admin_attrib = {} - for key, value in list(admin.items()): - if key == 'assignedTo': - admin_attrib['assignedToUser'] = user_from_userid(value) - elif key == 'roleId': - admin_attrib['role'] = role_from_roleid(value) - elif key == 'orgUnitId': - value = f'id:{value}' - admin_attrib['orgUnit'] = orgunit_from_orgunitid(value) - admin_attrib[key] = value - csvRows.append(admin_attrib) - display.write_csv_file(csvRows, titles, 'Admins', todrive) - -def buildOrgUnitIdToNameMap(): - cd = buildGAPIObject('directory') - result = gapi.call(cd.orgunits(), 'list', - customerId=GC_Values[GC_CUSTOMER_ID], - fields='organizationUnits(orgUnitPath,orgUnitId)', type='all') - GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME] = {} - for orgUnit in result['organizationUnits']: - GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME][orgUnit['orgUnitId']] = orgUnit['orgUnitPath'] - -def orgunit_from_orgunitid(orgunitid): - if not GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME]: - buildOrgUnitIdToNameMap() - return GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME].get(orgunitid, orgunitid) - -def buildRoleIdToNameToIdMap(): - cd = buildGAPIObject('directory') - result = gapi.get_all_pages(cd.roles(), 'list', 'items', - customer=GC_Values[GC_CUSTOMER_ID], - fields='nextPageToken,items(roleId,roleName)') - GM_Globals[GM_MAP_ROLE_ID_TO_NAME] = {} - GM_Globals[GM_MAP_ROLE_NAME_TO_ID] = {} - for role in result: - GM_Globals[GM_MAP_ROLE_ID_TO_NAME][role['roleId']] = role['roleName'] - GM_Globals[GM_MAP_ROLE_NAME_TO_ID][role['roleName']] = role['roleId'] - -def role_from_roleid(roleid): - if not GM_Globals[GM_MAP_ROLE_ID_TO_NAME]: - buildRoleIdToNameToIdMap() - return GM_Globals[GM_MAP_ROLE_ID_TO_NAME].get(roleid, roleid) - -def roleid_from_role(role): - if not GM_Globals[GM_MAP_ROLE_NAME_TO_ID]: - buildRoleIdToNameToIdMap() - return GM_Globals[GM_MAP_ROLE_NAME_TO_ID].get(role, None) - -def getRoleId(role): - cg = UID_PATTERN.match(role) - if cg: - roleId = cg.group(1) - else: - roleId = roleid_from_role(role) - if not roleId: - controlflow.system_error_exit(4, f'{role} is not a valid role. Please ensure role name is exactly as shown in admin console.') - return roleId - -def buildUserIdToNameMap(): - cd = buildGAPIObject('directory') - result = gapi.get_all_pages(cd.users(), 'list', 'users', - customer=GC_Values[GC_CUSTOMER_ID], - fields='nextPageToken,users(id,primaryEmail)') - GM_Globals[GM_MAP_USER_ID_TO_NAME] = {} - for user in result: - GM_Globals[GM_MAP_USER_ID_TO_NAME][user['id']] = user['primaryEmail'] - -def user_from_userid(userid): - if not GM_Globals[GM_MAP_USER_ID_TO_NAME]: - buildUserIdToNameMap() - return GM_Globals[GM_MAP_USER_ID_TO_NAME].get(userid, '') - -def appID2app(dt, appID): - for serviceName, serviceID in list(SERVICE_NAME_TO_ID_MAP.items()): - if appID == serviceID: - return serviceName - online_services = gapi.get_all_pages(dt.applications(), 'list', 'applications', customerId=GC_Values[GC_CUSTOMER_ID]) - for online_service in online_services: - if appID == online_service['id']: - return online_service['name'] - return f'applicationId: {appID}' - -def app2appID(dt, app): - serviceName = app.lower() - if serviceName in SERVICE_NAME_CHOICES_MAP: - return (SERVICE_NAME_CHOICES_MAP[serviceName], SERVICE_NAME_TO_ID_MAP[SERVICE_NAME_CHOICES_MAP[serviceName]]) - online_services = gapi.get_all_pages(dt.applications(), 'list', 'applications', customerId=GC_Values[GC_CUSTOMER_ID]) - for online_service in online_services: - if serviceName == online_service['name'].lower(): - return (online_service['name'], online_service['id']) - controlflow.system_error_exit(2, f'{app} is not a valid service for data transfer.') - -def convertToUserID(user): - cg = UID_PATTERN.match(user) - if cg: - return cg.group(1) - cd = buildGAPIObject('directory') - if user.find('@') == -1: - user = f'{user}@{GC_Values[GC_DOMAIN]}' - try: - return gapi.call(cd.users(), 'get', throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.FORBIDDEN], userKey=user, fields='id')['id'] - except (gapi.errors.GapiUserNotFoundError, gapi.errors.GapiBadRequestError, gapi.errors.GapiForbiddenError): - controlflow.system_error_exit(3, f'no such user {user}') - -def convertUserIDtoEmail(uid): - cd = buildGAPIObject('directory') - try: - return gapi.call(cd.users(), 'get', throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.FORBIDDEN], userKey=uid, fields='primaryEmail')['primaryEmail'] - except (gapi.errors.GapiUserNotFoundError, gapi.errors.GapiBadRequestError, gapi.errors.GapiForbiddenError): - return f'uid:{uid}' - -def doCreateDataTransfer(): - dt = buildGAPIObject('datatransfer') - body = {} - old_owner = sys.argv[3] - body['oldOwnerUserId'] = convertToUserID(old_owner) - apps = sys.argv[4].split(",") - appNameList = [] - appIDList = [] - i = 0 - while i < len(apps): - serviceName, serviceID = app2appID(dt, apps[i]) - appNameList.append(serviceName) - appIDList.append({'applicationId': serviceID}) - i += 1 - body['applicationDataTransfers'] = (appIDList) - new_owner = sys.argv[5] - body['newOwnerUserId'] = convertToUserID(new_owner) - parameters = {} - i = 6 - while i < len(sys.argv): - parameters[sys.argv[i].upper()] = sys.argv[i+1].upper().split(',') - i += 2 - i = 0 - for key, value in list(parameters.items()): - body['applicationDataTransfers'][i].setdefault('applicationTransferParams', []) - body['applicationDataTransfers'][i]['applicationTransferParams'].append({'key': key, 'value': value}) - i += 1 - result = gapi.call(dt.transfers(), 'insert', body=body, fields='id')['id'] - print(f'Submitted request id {result} to transfer {",".join(map(str, appNameList))} from {old_owner} to {new_owner}') - -def doPrintTransferApps(): - dt = buildGAPIObject('datatransfer') - apps = gapi.get_all_pages(dt.applications(), 'list', 'applications', customerId=GC_Values[GC_CUSTOMER_ID]) - display.print_json(apps) - -def doPrintDataTransfers(): - dt = buildGAPIObject('datatransfer') - i = 3 - newOwnerUserId = None - oldOwnerUserId = None - status = None - todrive = False - titles = ['id',] - csvRows = [] - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg in ['olduser', 'oldowner']: - oldOwnerUserId = convertToUserID(sys.argv[i+1]) - i += 2 - elif myarg in ['newuser', 'newowner']: - newOwnerUserId = convertToUserID(sys.argv[i+1]) - i += 2 - elif myarg == 'status': - status = sys.argv[i+1] - i += 2 - elif myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print transfers") - transfers = gapi.get_all_pages(dt.transfers(), 'list', 'dataTransfers', - customerId=GC_Values[GC_CUSTOMER_ID], status=status, - newOwnerUserId=newOwnerUserId, oldOwnerUserId=oldOwnerUserId) - for transfer in transfers: - for i in range(0, len(transfer['applicationDataTransfers'])): - a_transfer = {} - a_transfer['oldOwnerUserEmail'] = convertUserIDtoEmail(transfer['oldOwnerUserId']) - a_transfer['newOwnerUserEmail'] = convertUserIDtoEmail(transfer['newOwnerUserId']) - a_transfer['requestTime'] = transfer['requestTime'] - a_transfer['applicationId'] = transfer['applicationDataTransfers'][i]['applicationId'] - a_transfer['application'] = appID2app(dt, a_transfer['applicationId']) - a_transfer['status'] = transfer['applicationDataTransfers'][i]['applicationTransferStatus'] - a_transfer['id'] = transfer['id'] - if 'applicationTransferParams' in transfer['applicationDataTransfers'][i]: - for param in transfer['applicationDataTransfers'][i]['applicationTransferParams']: - a_transfer[param['key']] = ','.join(param.get('value', [])) - for title in a_transfer: - if title not in titles: - titles.append(title) - csvRows.append(a_transfer) - display.write_csv_file(csvRows, titles, 'Data Transfers', todrive) - -def doGetDataTransferInfo(): - dt = buildGAPIObject('datatransfer') - dtId = sys.argv[3] - transfer = gapi.call(dt.transfers(), 'get', dataTransferId=dtId) - print(f'Old Owner: {convertUserIDtoEmail(transfer["oldOwnerUserId"])}') - print(f'New Owner: {convertUserIDtoEmail(transfer["newOwnerUserId"])}') - print(f'Request Time: {transfer["requestTime"]}') - for app in transfer['applicationDataTransfers']: - print(f'Application: {appID2app(dt, app["applicationId"])}') - print(f'Status: {app["applicationTransferStatus"]}') - print('Parameters:') - if 'applicationTransferParams' in app: - for param in app['applicationTransferParams']: - print(f' {param["key"]}: {",".join(param.get("value", []))}') - else: - print(' None') - print() - -def doPrintShowGuardians(csvFormat): - croom = buildGAPIObject('classroom') - invitedEmailAddress = None - studentIds = ['-',] - states = None - service = croom.userProfiles().guardians() - items = 'guardians' - itemName = 'Guardians' - if csvFormat: - csvRows = [] - todrive = False - titles = ['studentEmail', 'studentId', 'invitedEmailAddress', 'guardianId'] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'invitedguardian': - invitedEmailAddress = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg == 'student': - studentIds = [normalizeStudentGuardianEmailAddressOrUID(sys.argv[i+1])] - i += 2 - elif myarg == 'invitations': - service = croom.userProfiles().guardianInvitations() - items = 'guardianInvitations' - itemName = 'Guardian Invitations' - titles = ['studentEmail', 'studentId', 'invitedEmailAddress', 'invitationId'] - if states is None: - states = ['COMPLETE', 'PENDING', 'GUARDIAN_INVITATION_STATE_UNSPECIFIED'] - i += 1 - elif myarg == 'states': - states = sys.argv[i+1].upper().replace(',', ' ').split() - i += 2 - elif myarg in usergroup_types: - studentIds = getUsersToModify(entity_type=myarg, entity=sys.argv[i+1]) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {['show', 'print'][csvFormat]} guardians") - i = 0 - count = len(studentIds) - for studentId in studentIds: - i += 1 - studentId = normalizeStudentGuardianEmailAddressOrUID(studentId) - kwargs = {'invitedEmailAddress': invitedEmailAddress, 'studentId': studentId} - if items == 'guardianInvitations': - kwargs['states'] = states - if studentId != '-': - if csvFormat: - sys.stderr.write('\r') - sys.stderr.flush() - sys.stderr.write(f'Getting {itemName} for {studentId}{currentCount(i, count)}{" " * 40}') - guardians = gapi.get_all_pages(service, 'list', items, soft_errors=True, **kwargs) - if not csvFormat: - print(f'Student: {studentId}, {itemName}:{currentCount(i, count)}') - for guardian in guardians: - display.print_json(guardian, spacing=' ') - else: - for guardian in guardians: - guardian['studentEmail'] = studentId - display.add_row_titles_to_csv_file(utils.flatten_json(guardian), csvRows, titles) - if csvFormat: - sys.stderr.write('\n') - display.write_csv_file(csvRows, titles, itemName, todrive) - -def doInviteGuardian(): - croom = buildGAPIObject('classroom') - body = {'invitedEmailAddress': normalizeEmailAddressOrUID(sys.argv[3])} - studentId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[4]) - result = gapi.call(croom.userProfiles().guardianInvitations(), 'create', studentId=studentId, body=body) - print(f'Invited email {result["invitedEmailAddress"]} as guardian of {studentId}. Invite ID {result["invitationId"]}') - -def _cancelGuardianInvitation(croom, studentId, invitationId): - try: - result = gapi.call(croom.userProfiles().guardianInvitations(), 'patch', - throw_reasons=[gapi.errors.ErrorReason.FAILED_PRECONDITION, gapi.errors.ErrorReason.FORBIDDEN, gapi.errors.ErrorReason.NOT_FOUND], - studentId=studentId, invitationId=invitationId, updateMask='state', body={'state': 'COMPLETE'}) - print(f'Cancelled PENDING guardian invitation for {result["invitedEmailAddress"]} as guardian of {studentId}') - return True - except gapi.errors.GapiFailedPreconditionError: - display.print_error(f'Guardian invitation {invitationId} for {studentId} status is not PENDING') - GM_Globals[GM_SYSEXITRC] = 3 - return True - except gapi.errors.GapiForbiddenError: - entityUnknownWarning('Student', studentId, 0, 0) - sys.exit(3) - except gapi.errors.GapiNotFoundError: - return False - -def doCancelGuardianInvitation(): - croom = buildGAPIObject('classroom') - invitationId = sys.argv[3] - studentId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[4]) - if not _cancelGuardianInvitation(croom, studentId, invitationId): - controlflow.system_error_exit(3, f'Guardian invitation {invitationId} for {studentId} does not exist') - -def _deleteGuardian(croom, studentId, guardianId, guardianEmail): - try: - gapi.call(croom.userProfiles().guardians(), 'delete', - throw_reasons=[gapi.errors.ErrorReason.FORBIDDEN, gapi.errors.ErrorReason.NOT_FOUND], - studentId=studentId, guardianId=guardianId) - print(f'Deleted {guardianEmail} as a guardian of {studentId}') - return True - except gapi.errors.GapiForbiddenError: - entityUnknownWarning('Student', studentId, 0, 0) - sys.exit(3) - except gapi.errors.GapiNotFoundError: - return False - -def doDeleteGuardian(): - croom = buildGAPIObject('classroom') - invitationsOnly = False - guardianId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[3]) - guardianIdIsEmail = guardianId.find('@') != -1 - studentId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[4]) - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['invitation', 'invitations']: - invitationsOnly = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam delete guardian") - if not invitationsOnly: - if guardianIdIsEmail: - try: - results = gapi.get_all_pages(croom.userProfiles().guardians(), 'list', 'guardians', - throw_reasons=[gapi.errors.ErrorReason.FORBIDDEN], - studentId=studentId, invitedEmailAddress=guardianId, - fields='nextPageToken,guardians(studentId,guardianId)') - if results: - for result in results: - _deleteGuardian(croom, result['studentId'], result['guardianId'], guardianId) - return - except gapi.errors.GapiForbiddenError: - entityUnknownWarning('Student', studentId, 0, 0) - sys.exit(3) - else: - if _deleteGuardian(croom, studentId, guardianId, guardianId): - return - # See if there's a pending invitation - if guardianIdIsEmail: - try: - results = gapi.get_all_pages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations', - throw_reasons=[gapi.errors.ErrorReason.FORBIDDEN], - studentId=studentId, invitedEmailAddress=guardianId, states=['PENDING',], - fields='nextPageToken,guardianInvitations(studentId,invitationId)') - if results: - for result in results: - status = _cancelGuardianInvitation(croom, result['studentId'], result['invitationId']) - sys.exit(status) - except gapi.errors.GapiForbiddenError: - entityUnknownWarning('Student', studentId, 0, 0) - sys.exit(3) - else: - if _cancelGuardianInvitation(croom, studentId, guardianId): - return - controlflow.system_error_exit(3, f'{guardianId} is not a guardian of {studentId} and no invitation exists.') - -def doCreateCourse(): - croom = buildGAPIObject('classroom') - body = {} - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['alias', 'id']: - body['id'] = f'd:{sys.argv[i+1]}' - i += 2 - else: - getCourseAttribute(myarg, sys.argv[i+1], body, croom, 'create') - i += 2 - if 'ownerId' not in body: - controlflow.system_error_exit(2, 'expected teacher )') - if 'name' not in body: - controlflow.system_error_exit(2, 'expected name )') - result = gapi.call(croom.courses(), 'create', body=body) - print(f'Created course {result["id"]}') - -def doGetCourseInfo(): - croom = buildGAPIObject('classroom') - courseId = addCourseIdScope(sys.argv[3]) - info = gapi.call(croom.courses(), 'get', id=courseId) - info['ownerEmail'] = convertUIDtoEmailAddress(f'uid:{info["ownerId"]}') - display.print_json(info) - teachers = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', courseId=courseId) - students = gapi.get_all_pages(croom.courses().students(), 'list', 'students', courseId=courseId) - try: - aliases = gapi.get_all_pages(croom.courses().aliases(), 'list', 'aliases', throw_reasons=[gapi.errors.ErrorReason.NOT_IMPLEMENTED], courseId=courseId) - except gapi.errors.GapiNotImplementedError: - aliases = [] - if aliases: - print('Aliases:') - for alias in aliases: - print(f' {alias["alias"][2:]}') - print('Participants:') - print(' Teachers:') - for teacher in teachers: - try: - print(f' {teacher["profile"]["name"]["fullName"]} - {teacher["profile"]["emailAddress"]}') - except KeyError: - print(f' {teacher["profile"]["name"]["fullName"]}') - print(' Students:') - for student in students: - try: - print(f' {student["profile"]["name"]["fullName"]} - {student["profile"]["emailAddress"]}') - except KeyError: - print(f' {student["profile"]["name"]["fullName"]}') - -COURSE_ARGUMENT_TO_PROPERTY_MAP = { - 'alternatelink': 'alternateLink', - 'coursegroupemail': 'courseGroupEmail', - 'coursematerialsets': 'courseMaterialSets', - 'coursestate': 'courseState', - 'creationtime': 'creationTime', - 'description': 'description', - 'descriptionheading': 'descriptionHeading', - 'enrollmentcode': 'enrollmentCode', - 'guardiansenabled': 'guardiansEnabled', - 'id': 'id', - 'name': 'name', - 'ownerid': 'ownerId', - 'room': 'room', - 'section': 'section', - 'teacherfolder': 'teacherFolder', - 'teachergroupemail': 'teacherGroupEmail', - 'updatetime': 'updateTime', - } - -def doPrintCourses(): - - def _processFieldsList(myarg, i, fList): - fieldNameList = sys.argv[i+1] - for field in fieldNameList.lower().replace(',', ' ').split(): - if field in COURSE_ARGUMENT_TO_PROPERTY_MAP: - if field != 'id': - fList.append(COURSE_ARGUMENT_TO_PROPERTY_MAP[field]) - else: - controlflow.invalid_argument_exit(field, f"gam print courses {myarg}") - - def _saveParticipants(course, participants, role): - jcount = len(participants) - course[role] = jcount - display.add_titles_to_csv_file([role], titles) - if countsOnly: - return - j = 0 - for member in participants: - memberTitles = [] - prefix = f'{role}.{j}.' - profile = member['profile'] - emailAddress = profile.get('emailAddress') - if emailAddress: - memberTitle = prefix+'emailAddress' - course[memberTitle] = emailAddress - memberTitles.append(memberTitle) - memberId = profile.get('id') - if memberId: - memberTitle = prefix+'id' - course[memberTitle] = memberId - memberTitles.append(memberTitle) - fullName = profile.get('name', {}).get('fullName') - if fullName: - memberTitle = prefix+'name.fullName' - course[memberTitle] = fullName - memberTitles.append(memberTitle) - display.add_titles_to_csv_file(memberTitles, titles) - j += 1 - - croom = buildGAPIObject('classroom') - todrive = False - fieldsList = [] - skipFieldsList = [] - titles = ['id',] - csvRows = [] - ownerEmails = studentId = teacherId = None - courseStates = [] - countsOnly = showAliases = False - delimiter = ' ' - showMembers = '' - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'teacher': - teacherId = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg == 'student': - studentId = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg in ['state', 'states', 'status']: - _getCourseStates(croom, sys.argv[i+1], courseStates) - i += 2 - elif myarg == 'todrive': - todrive = True - i += 1 - elif myarg in ['alias', 'aliases']: - showAliases = True - i += 1 - elif myarg == 'countsonly': - countsOnly = True - i += 1 - elif myarg == 'delimiter': - delimiter = sys.argv[i+1] - i += 2 - elif myarg == 'show': - showMembers = sys.argv[i+1].lower() - validShows = ['all', 'students', 'teachers'] - if showMembers not in validShows: - controlflow.expected_argument_exit("show", ", ".join(validShows), showMembers) - i += 2 - elif myarg == 'fields': - if not fieldsList: - fieldsList = ['id',] - _processFieldsList(myarg, i, fieldsList) - i += 2 - elif myarg == 'skipfields': - _processFieldsList(myarg, i, skipFieldsList) - i += 2 - elif myarg == 'owneremail': - ownerEmails = {} - cd = buildGAPIObject('directory') - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print courses") - if ownerEmails is not None and fieldsList: - fieldsList.append('ownerId') - fields = f'nextPageToken,courses({",".join(set(fieldsList))})' if fieldsList else None - printGettingAllItems('Courses', None) - page_message = gapi.got_total_items_msg('Courses', '...\n') - all_courses = gapi.get_all_pages(croom.courses(), 'list', 'courses', page_message=page_message, teacherId=teacherId, studentId=studentId, courseStates=courseStates, fields=fields) - for course in all_courses: - if ownerEmails is not None: - ownerId = course['ownerId'] - if ownerId not in ownerEmails: - ownerEmails[ownerId] = convertUIDtoEmailAddress(f'uid{ownerId}', cd=cd) - course['ownerEmail'] = ownerEmails[ownerId] - for field in skipFieldsList: - course.pop(field, None) - display.add_row_titles_to_csv_file(utils.flatten_json(course), csvRows, titles) - if showAliases or showMembers: - if showAliases: - titles.append('Aliases') - if showMembers: - if countsOnly: - teachersFields = 'nextPageToken,teachers(profile(id))' - studentsFields = 'nextPageToken,students(profile(id))' - else: - teachersFields = 'nextPageToken,teachers(profile)' - studentsFields = 'nextPageToken,students(profile)' - i = 0 - count = len(csvRows) - for course in csvRows: - i += 1 - courseId = course['id'] - if showAliases: - alias_message = gapi.got_total_items_msg(f'Aliases for course {courseId}{currentCount(i, count)}', '') - course_aliases = gapi.get_all_pages(croom.courses().aliases(), 'list', 'aliases', - page_message=alias_message, - courseId=courseId) - course['Aliases'] = delimiter.join([alias['alias'][2:] for alias in course_aliases]) - if showMembers: - if showMembers != 'students': - teacher_message = gapi.got_total_items_msg(f'Teachers for course {courseId}{currentCount(i, count)}', '') - results = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', - page_message=teacher_message, - courseId=courseId, fields=teachersFields) - _saveParticipants(course, results, 'teachers') - if showMembers != 'teachers': - student_message = gapi.got_total_items_msg(f'Students for course {courseId}{currentCount(i, count)}', '') - results = gapi.get_all_pages(croom.courses().students(), 'list', 'students', - page_message=student_message, - courseId=courseId, fields=studentsFields) - _saveParticipants(course, results, 'students') - display.sort_csv_titles(['id', 'name'], titles) - display.write_csv_file(csvRows, titles, 'Courses', todrive) - -def doPrintCourseParticipants(): - croom = buildGAPIObject('classroom') - todrive = False - titles = ['courseId',] - csvRows = [] - courses = [] - teacherId = None - studentId = None - courseStates = [] - showMembers = 'all' - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['course', 'class']: - courses.append(addCourseIdScope(sys.argv[i+1])) - i += 2 - elif myarg == 'teacher': - teacherId = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg == 'student': - studentId = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg in ['state', 'states', 'status']: - _getCourseStates(croom, sys.argv[i+1], courseStates) - i += 2 - elif myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'show': - showMembers = sys.argv[i+1].lower() - validShows = ['all', 'students', 'teachers'] - if showMembers not in validShows: - controlflow.expected_argument_exit("show", ", ".join(validShows), showMembers) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print course-participants") - if not courses: - printGettingAllItems('Courses', None) - page_message = gapi.got_total_items_msg('Courses', '...\n') - all_courses = gapi.get_all_pages(croom.courses(), 'list', 'courses', page_message=page_message, - teacherId=teacherId, studentId=studentId, courseStates=courseStates, fields='nextPageToken,courses(id,name)') - else: - all_courses = [] - for course in courses: - all_courses.append(gapi.call(croom.courses(), 'get', id=course, fields='id,name')) - i = 0 - count = len(all_courses) - for course in all_courses: - i += 1 - courseId = course['id'] - if showMembers != 'students': - page_message = gapi.got_total_items_msg(f'Teachers for course {courseId}{currentCount(i, count)}', '') - teachers = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', page_message=page_message, courseId=courseId) - for teacher in teachers: - display.add_row_titles_to_csv_file(utils.flatten_json(teacher, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'TEACHER'}), csvRows, titles) - if showMembers != 'teachers': - page_message = gapi.got_total_items_msg(f'Students for course {courseId}{currentCount(i, count)}', '') - students = gapi.get_all_pages(croom.courses().students(), 'list', 'students', page_message=page_message, courseId=courseId) - for student in students: - display.add_row_titles_to_csv_file(utils.flatten_json(student, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'STUDENT'}), csvRows, titles) - display.sort_csv_titles(['courseId', 'courseName', 'userRole', 'userId'], titles) - display.write_csv_file(csvRows, titles, 'Course Participants', todrive) - -def doPrintPrintJobs(): - cp = buildGAPIObject('cloudprint') - todrive = False - titles = ['printerid', 'id'] - csvRows = [] - printerid = None - owner = None - status = None - sortorder = None - descending = False - query = None - age = None - older_or_newer = None - jobLimit = PRINTJOBS_DEFAULT_JOB_LIMIT - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg in ['olderthan', 'newerthan']: - if myarg == 'olderthan': - older_or_newer = 'older' - else: - older_or_newer = 'newer' - age_number = sys.argv[i+1][:-1] - if not age_number.isdigit(): - controlflow.system_error_exit(2, f'expected a number; got {age_number}') - age_unit = sys.argv[i+1][-1].lower() - if age_unit == 'm': - age = int(time.time()) - (int(age_number) * 60) - elif age_unit == 'h': - age = int(time.time()) - (int(age_number) * 60 * 60) - elif age_unit == 'd': - age = int(time.time()) - (int(age_number) * 60 * 60 * 24) - else: - controlflow.system_error_exit(2, f'expected m (minutes), h (hours) or d (days); got {age_unit}') - i += 2 - elif myarg == 'query': - query = sys.argv[i+1] - i += 2 - elif myarg == 'status': - status = sys.argv[i+1] - i += 2 - elif myarg == 'ascending': - descending = False - i += 1 - elif myarg == 'descending': - descending = True - i += 1 - elif myarg == 'orderby': - sortorder = sys.argv[i+1].lower().replace('_', '') - if sortorder not in PRINTJOB_ASCENDINGORDER_MAP: - controlflow.expected_argument_exit("orderby", ", ".join(PRINTJOB_ASCENDINGORDER_MAP), sortorder) - sortorder = PRINTJOB_ASCENDINGORDER_MAP[sortorder] - i += 2 - elif myarg in ['printer', 'printerid']: - printerid = sys.argv[i+1] - i += 2 - elif myarg in ['owner', 'user']: - owner = sys.argv[i+1] - i += 2 - elif myarg == 'limit': - jobLimit = getInteger(sys.argv[i+1], myarg, minVal=0) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print printjobs") - if sortorder and descending: - sortorder = PRINTJOB_DESCENDINGORDER_MAP[sortorder] - if printerid: - result = gapi.call(cp.printers(), 'get', - printerid=printerid) - checkCloudPrintResult(result) - if ((not sortorder) or (sortorder == 'CREATE_TIME_DESC')) and (older_or_newer == 'newer'): - timeExit = True - elif (sortorder == 'CREATE_TIME') and (older_or_newer == 'older'): - timeExit = True - else: - timeExit = False - jobCount = offset = 0 - while True: - if jobLimit == 0: - limit = PRINTJOBS_DEFAULT_MAX_RESULTS - else: - limit = min(PRINTJOBS_DEFAULT_MAX_RESULTS, jobLimit-jobCount) - if limit == 0: - break - result = gapi.call(cp.jobs(), 'list', - printerid=printerid, q=query, status=status, sortorder=sortorder, - owner=owner, offset=offset, limit=limit) - checkCloudPrintResult(result) - newJobs = result['range']['jobsCount'] - totalJobs = int(result['range']['jobsTotal']) - if GC_Values[GC_DEBUG_LEVEL] > 0: - sys.stderr.write(f'Debug: jobCount: {jobCount}, jobLimit: {jobLimit}, jobsCount: {newJobs}, jobsTotal: {totalJobs}\n') - if newJobs == 0: - break - jobCount += newJobs - offset += newJobs - for job in result['jobs']: - createTime = int(job['createTime'])/1000 - if older_or_newer: - if older_or_newer == 'older' and createTime > age: - if timeExit: - jobCount = totalJobs - break - continue - if older_or_newer == 'newer' and createTime < age: - if timeExit: - jobCount = totalJobs - break - continue - job['createTime'] = utils.formatTimestampYMDHMS(job['createTime']) - job['updateTime'] = utils.formatTimestampYMDHMS(job['updateTime']) - job['tags'] = ' '.join(job['tags']) - display.add_row_titles_to_csv_file(utils.flatten_json(job), csvRows, titles) - if jobCount >= totalJobs: - break - display.write_csv_file(csvRows, titles, 'Print Jobs', todrive) - -def doPrintPrinters(): - cp = buildGAPIObject('cloudprint') - todrive = False - titles = ['id',] - csvRows = [] - queries = [None] - printer_type = None - connection_status = None - extra_fields = None - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg in ['query', 'queries']: - queries = getQueries(myarg, sys.argv[i+1]) - i += 2 - elif myarg == 'type': - printer_type = sys.argv[i+1] - i += 2 - elif myarg == 'status': - connection_status = sys.argv[i+1] - i += 2 - elif myarg == 'extrafields': - extra_fields = sys.argv[i+1] - i += 2 - elif myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print printers") - for query in queries: - printers = gapi.call(cp.printers(), 'list', q=query, type=printer_type, connection_status=connection_status, extra_fields=extra_fields) - checkCloudPrintResult(printers) - for printer in printers['printers']: - printer['createTime'] = utils.formatTimestampYMDHMS(printer['createTime']) - printer['accessTime'] = utils.formatTimestampYMDHMS(printer['accessTime']) - printer['updateTime'] = utils.formatTimestampYMDHMS(printer['updateTime']) - printer['tags'] = ' '.join(printer['tags']) - display.add_row_titles_to_csv_file(utils.flatten_json(printer), csvRows, titles) - display.write_csv_file(csvRows, titles, 'Printers', todrive) - -def doPrinterShowACL(): - cp = buildGAPIObject('cloudprint') - show_printer = sys.argv[2] - printer_info = gapi.call(cp.printers(), 'get', printerid=show_printer) - checkCloudPrintResult(printer_info) - for acl in printer_info['printers'][0]['access']: - if 'key' in acl: - acl['accessURL'] = f'https://www.google.com/cloudprint/addpublicprinter.html?printerid={show_printer}&key={acl["key"]}' - display.print_json(acl) - print() - -def doPrinterAddACL(): - cp = buildGAPIObject('cloudprint') - printer = sys.argv[2] - role = sys.argv[4].upper() - scope = sys.argv[5] - notify = bool(len(sys.argv) > 6 and sys.argv[6].lower() == 'notify') - public = None - skip_notification = True - if scope.lower() == 'public': - public = True - scope = None - role = None - skip_notification = None - elif scope.find('@') == -1: - scope = f'/hd/domain/{scope}' - else: - skip_notification = not notify - result = gapi.call(cp.printers(), 'share', printerid=printer, role=role, scope=scope, public=public, skip_notification=skip_notification) - checkCloudPrintResult(result) - who = scope - if who is None: - who = 'public' - role = 'user' - print(f'Added {role} {who}') - -def doPrinterDelACL(): - cp = buildGAPIObject('cloudprint') - printer = sys.argv[2] - scope = sys.argv[4] - public = None - if scope.lower() == 'public': - public = True - scope = None - elif scope.find('@') == -1: - scope = f'/hd/domain/{scope}' - result = gapi.call(cp.printers(), 'unshare', printerid=printer, scope=scope, public=public) - checkCloudPrintResult(result) - who = scope - if who is None: - who = 'public' - print(f'Removed {who}') - -def encode_multipart(fields, files, boundary=None): - def escape_quote(s): - return s.replace('"', '\\"') - - def getFormDataLine(name, value, boundary): - return f'--{boundary}', f'Content-Disposition: form-data; name="{escape_quote(name)}"', '', str(value) - - if boundary is None: - boundary = ''.join(random.choice(ALPHANUMERIC_CHARS) for _ in range(30)) - lines = [] - for name, value in list(fields.items()): - if name == 'tags': - for tag in value: - lines.extend(getFormDataLine('tag', tag, boundary)) - else: - lines.extend(getFormDataLine(name, value, boundary)) - for name, value in list(files.items()): - filename = value['filename'] - mimetype = value['mimetype'] - lines.extend(( - f'--{boundary}', - f'Content-Disposition: form-data; name="{escape_quote(name)}"; filename="{escape_quote(filename)}"', - f'Content-Type: {mimetype}', - '', - value['content'], - )) - lines.extend(( - f'--{boundary}--', - '', - )) - body = '\r\n'.join(lines) - headers = { - 'Content-Type': f'multipart/form-data; boundary={boundary}', - 'Content-Length': str(len(body)), - } - return (body, headers) - -def doPrintJobFetch(): - cp = buildGAPIObject('cloudprint') - printerid = sys.argv[2] - if printerid == 'any': - printerid = None - owner = None - status = None - sortorder = None - descending = False - query = None - age = None - older_or_newer = None - jobLimit = PRINTJOBS_DEFAULT_JOB_LIMIT - targetFolder = os.getcwd() - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg in ['olderthan', 'newerthan']: - if myarg == 'olderthan': - older_or_newer = 'older' - else: - older_or_newer = 'newer' - age_number = sys.argv[i+1][:-1] - if not age_number.isdigit(): - controlflow.system_error_exit(2, f'expected a number; got {age_number}') - age_unit = sys.argv[i+1][-1].lower() - if age_unit == 'm': - age = int(time.time()) - (int(age_number) * 60) - elif age_unit == 'h': - age = int(time.time()) - (int(age_number) * 60 * 60) - elif age_unit == 'd': - age = int(time.time()) - (int(age_number) * 60 * 60 * 24) - else: - controlflow.system_error_exit(2, f'expected m (minutes), h (hours) or d (days); got {age_unit}') - i += 2 - elif myarg == 'query': - query = sys.argv[i+1] - i += 2 - elif myarg == 'status': - status = sys.argv[i+1] - i += 2 - elif myarg == 'ascending': - descending = False - i += 1 - elif myarg == 'descending': - descending = True - i += 1 - elif myarg == 'orderby': - sortorder = sys.argv[i+1].lower().replace('_', '') - if sortorder not in PRINTJOB_ASCENDINGORDER_MAP: - controlflow.expected_argument_exit("orderby", ", ".join(PRINTJOB_ASCENDINGORDER_MAP), sortorder) - sortorder = PRINTJOB_ASCENDINGORDER_MAP[sortorder] - i += 2 - elif myarg in ['owner', 'user']: - owner = sys.argv[i+1] - i += 2 - elif myarg == 'limit': - jobLimit = getInteger(sys.argv[i+1], myarg, minVal=0) - i += 2 - elif myarg == 'drivedir': - targetFolder = GC_Values[GC_DRIVE_DIR] - i += 1 - elif myarg == 'targetfolder': - targetFolder = os.path.expanduser(sys.argv[i+1]) - if not os.path.isdir(targetFolder): - os.makedirs(targetFolder) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam printjobs fetch") - if sortorder and descending: - sortorder = PRINTJOB_DESCENDINGORDER_MAP[sortorder] - if printerid: - result = gapi.call(cp.printers(), 'get', - printerid=printerid) - checkCloudPrintResult(result) - ssd = '{"state": {"type": "DONE"}}' - if ((not sortorder) or (sortorder == 'CREATE_TIME_DESC')) and (older_or_newer == 'newer'): - timeExit = True - elif (sortorder == 'CREATE_TIME') and (older_or_newer == 'older'): - timeExit = True - else: - timeExit = False - jobCount = offset = 0 - while True: - if jobLimit == 0: - limit = PRINTJOBS_DEFAULT_MAX_RESULTS - else: - limit = min(PRINTJOBS_DEFAULT_MAX_RESULTS, jobLimit-jobCount) - if limit == 0: - break - result = gapi.call(cp.jobs(), 'list', - printerid=printerid, q=query, status=status, sortorder=sortorder, - owner=owner, offset=offset, limit=limit) - checkCloudPrintResult(result) - newJobs = result['range']['jobsCount'] - totalJobs = int(result['range']['jobsTotal']) - if newJobs == 0: - break - jobCount += newJobs - offset += newJobs - for job in result['jobs']: - createTime = int(job['createTime'])/1000 - if older_or_newer: - if older_or_newer == 'older' and createTime > age: - if timeExit: - jobCount = totalJobs - break - continue - if older_or_newer == 'newer' and createTime < age: - if timeExit: - jobCount = totalJobs - break - continue - fileUrl = job['fileUrl'] - jobid = job['id'] - fileName = os.path.join(targetFolder, f'{"".join(c if c in FILENAME_SAFE_CHARS else "_" for c in job["title"])}-{jobid}') - _, content = cp._http.request(uri=fileUrl, method='GET') - if fileutils.write_file(fileName, content, mode='wb', continue_on_error=True): -# ticket = gapi.call(cp.jobs(), u'getticket', jobid=jobid, use_cjt=True) - result = gapi.call(cp.jobs(), 'update', jobid=jobid, semantic_state_diff=ssd) - checkCloudPrintResult(result) - print(f'Printed job {jobid} to {fileName}') - if jobCount >= totalJobs: - break - if jobCount == 0: - print('No print jobs.') - -def doDelPrinter(): - cp = buildGAPIObject('cloudprint') - printerid = sys.argv[3] - result = gapi.call(cp.printers(), 'delete', printerid=printerid) - checkCloudPrintResult(result) - -def doGetPrinterInfo(): - cp = buildGAPIObject('cloudprint') - printerid = sys.argv[3] - everything = False - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'everything': - everything = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam info printer") - result = gapi.call(cp.printers(), 'get', printerid=printerid) - checkCloudPrintResult(result) - printer_info = result['printers'][0] - printer_info['createTime'] = utils.formatTimestampYMDHMS(printer_info['createTime']) - printer_info['accessTime'] = utils.formatTimestampYMDHMS(printer_info['accessTime']) - printer_info['updateTime'] = utils.formatTimestampYMDHMS(printer_info['updateTime']) - printer_info['tags'] = ' '.join(printer_info['tags']) - if not everything: - del printer_info['capabilities'] - del printer_info['access'] - display.print_json(printer_info) - -def doUpdatePrinter(): - cp = buildGAPIObject('cloudprint') - printerid = sys.argv[3] - kwargs = {} - i = 4 - update_items = ['isTosAccepted', 'gcpVersion', 'setupUrl', - 'quotaEnabled', 'id', 'supportUrl', 'firmware', - 'currentQuota', 'type', 'public', 'status', 'description', - 'defaultDisplayName', 'proxy', 'dailyQuota', 'manufacturer', - 'displayName', 'name', 'uuid', 'updateUrl', 'ownerId', 'model'] - while i < len(sys.argv): - arg_in_item = False - for item in update_items: - if item.lower() == sys.argv[i].lower(): - kwargs[item] = sys.argv[i+1] - i += 2 - arg_in_item = True - break - if not arg_in_item: - controlflow.invalid_argument_exit(sys.argv[i], "gam update printer") - result = gapi.call(cp.printers(), 'update', printerid=printerid, **kwargs) - checkCloudPrintResult(result) - print(f'Updated printer {printerid}') - -def doPrinterRegister(): - cp = buildGAPIObject('cloudprint') - form_fields = {'name': 'GAM', - 'proxy': 'GAM', - 'uuid': _getValueFromOAuth('sub'), - 'manufacturer': gam_author, - 'model': 'cp1', - 'gcp_version': '2.0', - 'setup_url': GAM_URL, - 'support_url': 'https://groups.google.com/forum/#!forum/google-apps-manager', - 'update_url': GAM_RELEASES, - 'firmware': gam_version, - 'semantic_state': {"version": "1.0", "printer": {"state": "IDLE",}}, - 'use_cdd': True, - 'capabilities': {"version": "1.0", - "printer": {"supported_content_type": [{"content_type": "application/pdf", "min_version": "1.5"}, - {"content_type": "image/jpeg"}, - {"content_type": "text/plain"} - ], - "copies": {"default": 1, "max": 100}, - "media_size": {"option": [{"name": "ISO_A4", "width_microns": 210000, "height_microns": 297000}, - {"name": "NA_LEGAL", "width_microns": 215900, "height_microns": 355600}, - {"name": "NA_LETTER", "width_microns": 215900, "height_microns": 279400, "is_default": True} - ], - }, - }, - }, - 'tags': ['GAM', GAM_URL], - } - body, headers = encode_multipart(form_fields, {}) - #Get the printer first to make sure our OAuth access token is fresh - gapi.call(cp.printers(), 'list') - _, result = cp._http.request(uri='https://www.google.com/cloudprint/register', method='POST', body=body, headers=headers) - result = json.loads(result.decode(UTF8)) - checkCloudPrintResult(result) - print(f'Created printer {result["printers"][0]["id"]}') - -def doPrintJobResubmit(): - cp = buildGAPIObject('cloudprint') - jobid = sys.argv[2] - printerid = sys.argv[4] - ssd = '{"state": {"type": "HELD"}}' - result = gapi.call(cp.jobs(), 'update', jobid=jobid, semantic_state_diff=ssd) - checkCloudPrintResult(result) - ticket = gapi.call(cp.jobs(), 'getticket', jobid=jobid, use_cjt=True) - result = gapi.call(cp.jobs(), 'resubmit', printerid=printerid, jobid=jobid, ticket=ticket) - checkCloudPrintResult(result) - print(f'Success resubmitting {jobid} as job {result["job"]["id"]} to printer {printerid}') - -def doPrintJobSubmit(): - cp = buildGAPIObject('cloudprint') - printer = sys.argv[2] - content = sys.argv[4] - form_fields = {'printerid': printer, - 'title': content, - 'ticket': '{"version": "1.0"}', - 'tags': ['GAM', GAM_URL]} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'tag': - form_fields['tags'].append(sys.argv[i+1]) - i += 2 - elif myarg in ['name', 'title']: - form_fields['title'] = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam printer ... print") - form_files = {} - if content[:4] == 'http': - form_fields['content'] = content - form_fields['contentType'] = 'url' - else: - filepath = content - content = os.path.basename(content) - mimetype = mimetypes.guess_type(filepath)[0] - if mimetype is None: - mimetype = 'application/octet-stream' - filecontent = fileutils.read_file(filepath, mode='rb') - form_files['content'] = {'filename': content, 'content': filecontent, 'mimetype': mimetype} - #result = gapi.call(cp.printers(), u'submit', body=body) - body, headers = encode_multipart(form_fields, form_files) - #Get the printer first to make sure our OAuth access token is fresh - gapi.call(cp.printers(), 'get', printerid=printer) - _, result = cp._http.request(uri='https://www.google.com/cloudprint/submit', method='POST', body=body, headers=headers) - result = json.loads(result.decode(UTF8)) - checkCloudPrintResult(result) - print(f'Submitted print job {result["job"]["id"]}') - -def doDeletePrintJob(): - cp = buildGAPIObject('cloudprint') - job = sys.argv[2] - result = gapi.call(cp.jobs(), 'delete', jobid=job) - checkCloudPrintResult(result) - print(f'Print Job {job} deleted') - -def doCancelPrintJob(): - cp = buildGAPIObject('cloudprint') - job = sys.argv[2] - ssd = '{"state": {"type": "ABORTED", "user_action_cause": {"action_code": "CANCELLED"}}}' - result = gapi.call(cp.jobs(), 'update', jobid=job, semantic_state_diff=ssd) - checkCloudPrintResult(result) - print(f'Print Job {job} cancelled') - -def checkCloudPrintResult(result): - if isinstance(result, bytes): - result = result.decode(UTF8) - if isinstance(result, str): - try: - result = json.loads(result) - except ValueError: - controlflow.system_error_exit(3, f'unexpected response: {result}') - if not result['success']: - controlflow.system_error_exit(result['errorCode'], f'{result["errorCode"]}: {result["message"]}') - -def doProfile(users): - cd = buildGAPIObject('directory') - myarg = sys.argv[4].lower() - if myarg in ['share', 'shared']: - body = {'includeInGlobalAddressList': True} - elif myarg in ['unshare', 'unshared']: - body = {'includeInGlobalAddressList': False} - else: - controlflow.expected_argument_exit('value for "gam profile"', ", ".join(["share", "shared", "unshare", "unshared"]), sys.argv[4]) - i = 0 - count = len(users) - for user in users: - i += 1 - print(f'Setting Profile Sharing to {body["includeInGlobalAddressList"]} for {user}{currentCount(i, count)}') - gapi.call(cd.users(), 'update', soft_errors=True, userKey=user, body=body) - -def showProfile(users): - cd = buildGAPIObject('directory') - i = 0 - count = len(users) - for user in users: - i += 1 - result = gapi.call(cd.users(), 'get', userKey=user, fields='includeInGlobalAddressList') - try: - print(f'User: {user} Profile Shared: {result["includeInGlobalAddressList"]}{currentCount(i, count)}') - except IndexError: - pass - -def doPhoto(users): - cd = buildGAPIObject('directory') - i = 0 - count = len(users) - for user in users: - i += 1 - filename = sys.argv[5].replace('#user#', user) - filename = filename.replace('#email#', user) - filename = filename.replace('#username#', user[:user.find('@')]) - print(f'Updating photo for {user} with {filename}{currentCount(i, count)}') - if re.match('^(ht|f)tps?://.*$', filename): - simplehttp = transport.create_http() - try: - (_, image_data) = simplehttp.request(filename, 'GET') - except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError) as e: - print(e) - continue - else: - image_data = fileutils.read_file(filename, mode='rb', continue_on_error=True, display_errors=True) - if image_data is None: - continue - body = {'photoData': base64.urlsafe_b64encode(image_data).decode(UTF8)} - gapi.call(cd.users().photos(), 'update', soft_errors=True, userKey=user, body=body) - -def getPhoto(users): - cd = buildGAPIObject('directory') - targetFolder = os.getcwd() - showPhotoData = True - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'drivedir': - targetFolder = GC_Values[GC_DRIVE_DIR] - i += 1 - elif myarg == 'targetfolder': - targetFolder = os.path.expanduser(sys.argv[i+1]) - if not os.path.isdir(targetFolder): - os.makedirs(targetFolder) - i += 2 - elif myarg == 'noshow': - showPhotoData = False - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam get photo") - i = 0 - count = len(users) - for user in users: - i += 1 - filename = os.path.join(targetFolder, f'{user}.jpg') - print(f'Saving photo to {filename}{currentCount(i, count)}') - try: - photo = gapi.call(cd.users().photos(), 'get', throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.RESOURCE_NOT_FOUND], userKey=user) - except gapi.errors.GapiUserNotFoundError: - print(f' unknown user {user}') - continue - except gapi.errors.GapiResourceNotFoundError: - print(f' no photo for {user}') - continue - try: - photo_data = photo['photoData'] - if showPhotoData: - print(photo_data) - except KeyError: - print(f' no photo for {user}') - continue - decoded_photo_data = base64.urlsafe_b64decode(photo_data) - fileutils.write_file(filename, decoded_photo_data, mode='wb', continue_on_error=True) - -def deletePhoto(users): - cd = buildGAPIObject('directory') - i = 0 - count = len(users) - for user in users: - i += 1 - print(f'Deleting photo for {user}{currentCount(i, count)}') - gapi.call(cd.users().photos(), 'delete', userKey=user) - -def printDriveSettings(users): - todrive = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show drivesettings") - dont_show = ['kind', 'exportFormats', 'importFormats', 'maxUploadSize', 'maxImportSizes', 'user', 'appInstalled'] - csvRows = [] - titles = ['email',] - i = 0 - count = len(users) - for user in users: - i += 1 - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - sys.stderr.write(f'Getting Drive settings for {user}{currentCountNL(i, count)}') - feed = gapi.call(drive.about(), 'get', fields='*', soft_errors=True) - if feed is None: - continue - row = {'email': user} - for setting in feed: - if setting in dont_show: - continue - if setting == 'storageQuota': - for subsetting, value in feed[setting].items(): - row[subsetting] = f'{int(value) / 1024 / 1024}mb' - if subsetting not in titles: - titles.append(subsetting) - continue - row[setting] = feed[setting] - if setting not in titles: - titles.append(setting) - csvRows.append(row) - display.write_csv_file(csvRows, titles, 'User Drive Settings', todrive) - -def getTeamDriveThemes(users): - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - themes = gapi.call(drive.about(), 'get', fields='teamDriveThemes', soft_errors=True) - if themes is None or 'teamDriveThemes' not in themes: - continue - print('theme') - for theme in themes['teamDriveThemes']: - print(theme['id']) - -def printDriveActivity(users): - drive_ancestorId = 'root' - drive_fileId = None - todrive = False - titles = ['user.name', 'user.permissionId', 'target.id', 'target.name', 'target.mimeType'] - csvRows = [] - i = 5 - while i < len(sys.argv): - activity_object = sys.argv[i].lower().replace('_', '') - if activity_object == 'fileid': - drive_fileId = sys.argv[i+1] - drive_ancestorId = None - i += 2 - elif activity_object == 'folderid': - drive_ancestorId = sys.argv[i+1] - i += 2 - elif activity_object == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show driveactivity") - for user in users: - user, activity = buildActivityGAPIObject(user) - if not activity: - continue - page_message = gapi.got_total_items_msg(f'Activities for {user}', '') - feed = gapi.get_all_pages(activity.activities(), 'list', 'activities', - page_message=page_message, source='drive.google.com', userId='me', - drive_ancestorId=drive_ancestorId, groupingStrategy='none', - drive_fileId=drive_fileId) - for item in feed: - display.add_row_titles_to_csv_file(utils.flatten_json(item['combinedEvent']), csvRows, titles) - display.write_csv_file(csvRows, titles, 'Drive Activity', todrive) - -def printPermission(permission): - if 'name' in permission: - print(permission['name']) - elif 'id' in permission: - if permission['id'] == 'anyone': - print('Anyone') - elif permission['id'] == 'anyoneWithLink': - print('Anyone with Link') - else: - print(permission['id']) - for key in permission: - if key in ['name', 'kind', 'etag', 'selfLink',]: - continue - print(f' {key}: {permission[key]}') - -def showDriveFileACL(users): - fileId = sys.argv[5] - useDomainAdminAccess = False - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show drivefileacl") - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - feed = gapi.get_all_pages(drive.permissions(), 'list', 'permissions', - fileId=fileId, fields='*', supportsAllDrives=True, - useDomainAdminAccess=useDomainAdminAccess) - for permission in feed: - printPermission(permission) - print('') - -def getPermissionId(argstr): - permissionId = argstr.strip() - cg = UID_PATTERN.match(permissionId) - if cg: - return cg.group(1) - permissionId = argstr.lower() - if permissionId == 'anyone': - return 'anyone' - if permissionId == 'anyonewithlink': - return 'anyoneWithLink' - if permissionId.find('@') == -1: - permissionId = f'{permissionId}@{GC_Values[GC_DOMAIN].lower()}' - # We have to use v2 here since v3 has no permissions.getIdForEmail equivalent - # https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4313 - _, drive2 = buildDriveGAPIObject(_getValueFromOAuth('email')) - return gapi.call(drive2.permissions(), 'getIdForEmail', email=permissionId, fields='id')['id'] - -def delDriveFileACL(users): - fileId = sys.argv[5] - permissionId = getPermissionId(sys.argv[6]) - useDomainAdminAccess = False - i = 7 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam delete drivefileacl") - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - print(f'Removing permission for {permissionId} from {fileId}') - gapi.call(drive.permissions(), 'delete', fileId=fileId, - permissionId=permissionId, supportsAllDrives=True, - useDomainAdminAccess=useDomainAdminAccess) - -DRIVEFILE_ACL_ROLES_MAP = { - 'commenter': 'commenter', - 'contentmanager': 'fileOrganizer', - 'editor': 'writer', - 'fileorganizer': 'fileOrganizer', - 'organizer': 'organizer', - 'owner': 'owner', - 'read': 'reader', - 'reader': 'reader', - 'writer': 'writer', - } - -def addDriveFileACL(users): - fileId = sys.argv[5] - body = {'type': sys.argv[6].lower()} - sendNotificationEmail = False - emailMessage = None - transferOwnership = None - useDomainAdminAccess = False - if body['type'] == 'anyone': - i = 7 - elif body['type'] in ['user', 'group']: - body['emailAddress'] = normalizeEmailAddressOrUID(sys.argv[7]) - i = 8 - elif body['type'] == 'domain': - body['domain'] = sys.argv[7] - i = 8 - else: - controlflow.expected_argument_exit("permission type", ", ".join(["user", "group", "domain", "anyone"]), body['type']) - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'withlink': - body['allowFileDiscovery'] = False - i += 1 - elif myarg == 'discoverable': - body['allowFileDiscovery'] = True - i += 1 - elif myarg == 'role': - role = sys.argv[i+1].lower() - if role not in DRIVEFILE_ACL_ROLES_MAP: - controlflow.expected_argument_exit("role", ", ".join(DRIVEFILE_ACL_ROLES_MAP), role) - body['role'] = DRIVEFILE_ACL_ROLES_MAP[role] - if body['role'] == 'owner': - sendNotificationEmail = True - transferOwnership = True - i += 2 - elif myarg == 'sendemail': - sendNotificationEmail = True - i += 1 - elif myarg == 'emailmessage': - sendNotificationEmail = True - emailMessage = sys.argv[i+1] - i += 2 - elif myarg == 'expires': - body['expirationTime'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) - i += 2 - elif myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam add drivefileacl") - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - result = gapi.call(drive.permissions(), 'create', fields='*', - fileId=fileId, sendNotificationEmail=sendNotificationEmail, - emailMessage=emailMessage, body=body, supportsAllDrives=True, - transferOwnership=transferOwnership, - useDomainAdminAccess=useDomainAdminAccess) - printPermission(result) - -def updateDriveFileACL(users): - fileId = sys.argv[5] - permissionId = getPermissionId(sys.argv[6]) - transferOwnership = None - removeExpiration = None - useDomainAdminAccess = False - body = {} - i = 7 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'removeexpiration': - removeExpiration = True - i += 1 - elif myarg == 'role': - role = sys.argv[i+1].lower() - if role not in DRIVEFILE_ACL_ROLES_MAP: - controlflow.expected_argument_exit("role", ", ".join(DRIVEFILE_ACL_ROLES_MAP), role) - body['role'] = DRIVEFILE_ACL_ROLES_MAP[role] - if body['role'] == 'owner': - transferOwnership = True - i += 2 - elif myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam update drivefileacl") - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - print(f'updating permissions for {permissionId} to file {fileId}') - result = gapi.call(drive.permissions(), 'update', fields='*', - fileId=fileId, permissionId=permissionId, removeExpiration=removeExpiration, - transferOwnership=transferOwnership, body=body, - supportsAllDrives=True, useDomainAdminAccess=useDomainAdminAccess) - printPermission(result) - -def _stripMeInOwners(query): - if not query: - return query - if query == "'me' in owners": - return None - if query.startswith("'me' in owners and "): - return query[len("'me' in owners and "):] - return query - -def printDriveFileList(users): - allfields = anyowner = todrive = False - fieldsList = [] - fieldsTitles = {} - labelsList = [] - orderByList = [] - titles = ['Owner',] - csvRows = [] - query = "'me' in owners" - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'orderby': - fieldName = sys.argv[i+1].lower() - i += 2 - if fieldName in DRIVEFILE_ORDERBY_CHOICES_MAP: - fieldName = DRIVEFILE_ORDERBY_CHOICES_MAP[fieldName] - orderBy = '' - if i < len(sys.argv): - orderBy = sys.argv[i].lower() - if orderBy in SORTORDER_CHOICES_MAP: - orderBy = SORTORDER_CHOICES_MAP[orderBy] - i += 1 - if orderBy != 'DESCENDING': - orderByList.append(fieldName) - else: - orderByList.append(f'{fieldName} desc') - else: - controlflow.expected_argument_exit("orderby", ", ".join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)), fieldName) - elif myarg == 'query': - query += f' and {sys.argv[i+1]}' - i += 2 - elif myarg == 'fullquery': - query = sys.argv[i+1] - i += 2 - elif myarg == 'anyowner': - anyowner = True - i += 1 - elif myarg == 'allfields': - fieldsList = [] - allfields = True - i += 1 - elif myarg in DRIVEFILE_FIELDS_CHOICES_MAP: - display.add_field_to_csv_file(myarg, {myarg: [DRIVEFILE_FIELDS_CHOICES_MAP[myarg]]}, fieldsList, fieldsTitles, titles) - i += 1 - elif myarg in DRIVEFILE_LABEL_CHOICES_MAP: - display.add_field_to_csv_file(myarg, {myarg: [DRIVEFILE_LABEL_CHOICES_MAP[myarg]]}, labelsList, fieldsTitles, titles) - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam show filelist") - if fieldsList or labelsList: - fields = 'nextPageToken,items(' - if fieldsList: - fields += ','.join(set(fieldsList)) - if labelsList: - fields += ',' - if labelsList: - fields += f'labels({",".join(set(labelsList))})' - fields += ')' - elif not allfields: - for field in ['name', 'alternatelink']: - display.add_field_to_csv_file(field, {field: [DRIVEFILE_FIELDS_CHOICES_MAP[field]]}, fieldsList, fieldsTitles, titles) - fields = f'nextPageToken,items({",".join(set(fieldsList))})' - else: - fields = '*' - if orderByList: - orderBy = ','.join(orderByList) - else: - orderBy = None - if anyowner: - query = _stripMeInOwners(query) - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - sys.stderr.write(f'Getting files for {user}...\n') - page_message = gapi.got_total_items_msg(f'Files for {user}', '...\n') - feed = gapi.get_all_pages(drive.files(), 'list', 'items', - page_message=page_message, soft_errors=True, - q=query, orderBy=orderBy, fields=fields) - for f_file in feed: - a_file = {'Owner': user} - for attrib in f_file: - if attrib in ['kind', 'etag']: - continue - if not isinstance(f_file[attrib], dict): - if isinstance(f_file[attrib], list): - if f_file[attrib]: - if isinstance(f_file[attrib][0], (str, int, bool)): - if attrib not in titles: - titles.append(attrib) - a_file[attrib] = ' '.join(f_file[attrib]) - else: - for j, l_attrib in enumerate(f_file[attrib]): - for list_attrib in l_attrib: - if list_attrib in ['kind', 'etag', 'selfLink']: - continue - x_attrib = f'{attrib}.{j}.{list_attrib}' - if x_attrib not in titles: - titles.append(x_attrib) - a_file[x_attrib] = l_attrib[list_attrib] - elif isinstance(f_file[attrib], (str, int, bool)): - if attrib not in titles: - titles.append(attrib) - a_file[attrib] = f_file[attrib] - else: - sys.stderr.write(f'File ID: {f_file["id"]}, Attribute: {attrib}, Unknown type: {type(f_file[attrib])}\n') - elif attrib == 'labels': - for dict_attrib in f_file[attrib]: - if dict_attrib not in titles: - titles.append(dict_attrib) - a_file[dict_attrib] = f_file[attrib][dict_attrib] - else: - for dict_attrib in f_file[attrib]: - if dict_attrib in ['kind', 'etag']: - continue - x_attrib = f'{attrib}.{dict_attrib}' - if x_attrib not in titles: - titles.append(x_attrib) - a_file[x_attrib] = f_file[attrib][dict_attrib] - csvRows.append(a_file) - if allfields: - display.sort_csv_titles(['Owner', 'id', 'title'], titles) - display.write_csv_file(csvRows, titles, f'{sys.argv[1]} {sys.argv[2]} Drive Files', todrive) - -def doDriveSearch(drive, query=None, quiet=False): - if not quiet: - print(f'Searching for files with query: "{query}"...') - page_message = gapi.got_total_items_msg('Files', '...\n') - else: - page_message = None - files = gapi.get_all_pages(drive.files(), 'list', 'items', - page_message=page_message, - q=query, fields='nextPageToken,items(id)') - ids = list() - for f_file in files: - ids.append(f_file['id']) - return ids - -def getFileIdFromAlternateLink(altLink): - loc = altLink.find('/d/') - if loc > 0: - fileId = altLink[loc+3:] - loc = fileId.find('/') - if loc != -1: - return fileId[:loc] - else: - loc = altLink.find('/folderview?id=') - if loc > 0: - fileId = altLink[loc+15:] - loc = fileId.find('&') - if loc != -1: - return fileId[:loc] - controlflow.system_error_exit(2, f'{altLink} is not a valid Drive File alternateLink') - -def deleteDriveFile(users): - fileIds = sys.argv[5] - function = 'trash' - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'purge': - function = 'delete' - i += 1 - elif myarg == 'untrash': - function = 'untrash' - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam delete drivefile") - action = DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP[function] - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - if fileIds[:6].lower() == 'query:': - file_ids = doDriveSearch(drive, query=fileIds[6:]) - else: - if fileIds[:8].lower() == 'https://' or fileIds[:7].lower() == 'http://': - fileIds = getFileIdFromAlternateLink(fileIds) - file_ids = [fileIds,] - if not file_ids: - print(f'No files to {function} for {user}') - j = 0 - batch_size = 10 - dbatch = drive.new_batch_http_request(callback=drive_del_result) - method = getattr(drive.files(), function) - for fileId in file_ids: - j += 1 - dbatch.add(method(fileId=fileId, supportsAllDrives=True)) - if len(dbatch._order) == batch_size: - print(f'{action} {len(dbatch._order)} files...') - dbatch.execute() - dbatch = drive.new_batch_http_request(callback=drive_del_result) - if len(dbatch._order) > 0: - print(f'{action} {len(dbatch._order)} files...') - dbatch.execute() - -def drive_del_result(request_id, response, exception): - if exception: - print(exception) - -def printDriveFolderContents(feed, folderId, indent): - for f_file in feed: - for parent in f_file['parents']: - if folderId == parent['id']: - print(' ' * indent, f_file['title']) - if f_file['mimeType'] == 'application/vnd.google-apps.folder': - printDriveFolderContents(feed, f_file['id'], indent+1) - break - -def showDriveFileTree(users): - anyowner = False - orderByList = [] - query = "'me' in owners" - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'anyowner': - anyowner = True - i += 1 - elif myarg == 'orderby': - fieldName = sys.argv[i+1].lower() - i += 2 - if fieldName in DRIVEFILE_ORDERBY_CHOICES_MAP: - fieldName = DRIVEFILE_ORDERBY_CHOICES_MAP[fieldName] - orderBy = '' - if i < len(sys.argv): - orderBy = sys.argv[i].lower() - if orderBy in SORTORDER_CHOICES_MAP: - orderBy = SORTORDER_CHOICES_MAP[orderBy] - i += 1 - if orderBy != 'DESCENDING': - orderByList.append(fieldName) - else: - orderByList.append(f'{fieldName} desc') - else: - controlflow.expected_argument_exit("orderby", ", ".join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)), fieldName) - else: - controlflow.invalid_argument_exit(myarg, "gam show filetree") - if orderByList: - orderBy = ','.join(orderByList) - else: - orderBy = None - if anyowner: - query = _stripMeInOwners(query) - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - root_folder = gapi.call(drive.about(), 'get', fields='rootFolderId')['rootFolderId'] - sys.stderr.write(f'Getting all files for {user}...\n') - page_message = gapi.got_total_items_msg(f'Files for {user}', '...\n') - feed = gapi.get_all_pages(drive.files(), 'list', 'items', page_message=page_message, - q=query, orderBy=orderBy, fields='items(id,title,parents(id),mimeType),nextPageToken') - printDriveFolderContents(feed, root_folder, 0) - -def deleteEmptyDriveFolders(users): - query = '"me" in owners and mimeType = "application/vnd.google-apps.folder"' - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - deleted_empty = True - while deleted_empty: - sys.stderr.write(f'Getting folders for {user}...\n') - page_message = gapi.got_total_items_msg(f'Folders for {user}', '...\n') - feed = gapi.get_all_pages(drive.files(), 'list', 'items', page_message=page_message, - q=query, fields='items(title,id),nextPageToken') - deleted_empty = False - for folder in feed: - children = gapi.call(drive.children(), 'list', - folderId=folder['id'], fields='items(id)', maxResults=1) - if 'items' not in children or not children['items']: - print(f' deleting empty folder {folder["title"]}...') - gapi.call(drive.files(), 'delete', fileId=folder['id']) - deleted_empty = True - else: - print(f' not deleting folder {folder["title"]} because it contains at least 1 item ({children["items"][0]["id"]})') - -def doEmptyDriveTrash(users): - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - print(f'Emptying Drive trash for {user}') - gapi.call(drive.files(), 'emptyTrash') - -def escapeDriveFileName(filename): - if filename.find("'") == -1 and filename.find('\\') == -1: - return filename - encfilename = '' - for c in filename: - if c == "'": - encfilename += "\\'" - elif c == '\\': - encfilename += '\\\\' - else: - encfilename += c - return encfilename - -def initializeDriveFileAttributes(): - return ({}, {DFA_LOCALFILEPATH: None, DFA_LOCALFILENAME: None, DFA_LOCALMIMETYPE: None, DFA_CONVERT: None, DFA_OCR: None, DFA_OCRLANGUAGE: None, DFA_PARENTQUERY: None}) - -def getDriveFileAttribute(i, body, parameters, myarg, update=False): - if myarg == 'localfile': - parameters[DFA_LOCALFILEPATH] = sys.argv[i+1] - parameters[DFA_LOCALFILENAME] = os.path.basename(parameters[DFA_LOCALFILEPATH]) - body.setdefault('title', parameters[DFA_LOCALFILENAME]) - body['mimeType'] = mimetypes.guess_type(parameters[DFA_LOCALFILEPATH])[0] - if body['mimeType'] is None: - body['mimeType'] = 'application/octet-stream' - parameters[DFA_LOCALMIMETYPE] = body['mimeType'] - i += 2 - elif myarg == 'convert': - parameters[DFA_CONVERT] = True - i += 1 - elif myarg == 'ocr': - parameters[DFA_OCR] = True - i += 1 - elif myarg == 'ocrlanguage': - parameters[DFA_OCRLANGUAGE] = LANGUAGE_CODES_MAP.get(sys.argv[i+1].lower(), sys.argv[i+1]) - i += 2 - elif myarg in ['copyrequireswriterpermission', 'restrict', 'restricted']: - if update: - body['copyRequiresWriterPermission'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - else: - body['copyRequiresWriterPermission'] = True - i += 1 - elif myarg in DRIVEFILE_LABEL_CHOICES_MAP: - body.setdefault('labels', {}) - if update: - body['labels'][DRIVEFILE_LABEL_CHOICES_MAP[myarg]] = getBoolean(sys.argv[i+1], myarg) - i += 2 - else: - body['labels'][DRIVEFILE_LABEL_CHOICES_MAP[myarg]] = True - i += 1 - elif myarg in ['lastviewedbyme', 'lastviewedbyuser', 'lastviewedbymedate', 'lastviewedbymetime']: - body['lastViewedByMeDate'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) - i += 2 - elif myarg in ['modifieddate', 'modifiedtime']: - body['modifiedDate'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) - i += 2 - elif myarg == 'description': - body['description'] = sys.argv[i+1] - i += 2 - elif myarg == 'mimetype': - mimeType = sys.argv[i+1] - if mimeType in MIMETYPE_CHOICES_MAP: - body['mimeType'] = MIMETYPE_CHOICES_MAP[mimeType] - else: - controlflow.expected_argument_exit("mimetype", ", ".join(MIMETYPE_CHOICES_MAP), mimeType) - i += 2 - elif myarg == 'parentid': - body.setdefault('parents', []) - body['parents'].append({'id': sys.argv[i+1]}) - i += 2 - elif myarg == 'parentname': - parameters[DFA_PARENTQUERY] = f"'me' in owners and mimeType = '{MIMETYPE_GA_FOLDER}' and title = '{escapeDriveFileName(sys.argv[i+1])}'" - i += 2 - elif myarg in ['anyownerparentname']: - parameters[DFA_PARENTQUERY] = f"mimeType = '{MIMETYPE_GA_FOLDER}' and title = '{escapeDriveFileName(sys.argv[i+1])}'" - i += 2 - elif myarg == 'writerscantshare': - body['writersCanShare'] = False - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['add', 'update'][update]} drivefile") - return i - -def doUpdateDriveFile(users): - fileIdSelection = {'fileIds': [], 'query': None} - media_body = None - operation = 'update' - body, parameters = initializeDriveFileAttributes() - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'copy': - operation = 'copy' - i += 1 - elif myarg == 'newfilename': - body['title'] = sys.argv[i+1] - i += 2 - elif myarg == 'id': - fileIdSelection['fileIds'] = [sys.argv[i+1],] - i += 2 - elif myarg == 'query': - fileIdSelection['query'] = sys.argv[i+1] - i += 2 - elif myarg == 'drivefilename': - fileIdSelection['query'] = f"'me' in owners and title = '{sys.argv[i+1]}'" - i += 2 - else: - i = getDriveFileAttribute(i, body, parameters, myarg, True) - if not fileIdSelection['query'] and not fileIdSelection['fileIds']: - controlflow.system_error_exit(2, 'you need to specify either id, query or drivefilename in order to determine the file(s) to update') - if fileIdSelection['query'] and fileIdSelection['fileIds']: - controlflow.system_error_exit(2, 'you cannot specify multiple file identifiers. Choose one of id, drivefilename, query.') - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - if parameters[DFA_PARENTQUERY]: - more_parents = doDriveSearch(drive, query=parameters[DFA_PARENTQUERY]) - body.setdefault('parents', []) - for a_parent in more_parents: - body['parents'].append({'id': a_parent}) - if fileIdSelection['query']: - fileIdSelection['fileIds'] = doDriveSearch(drive, query=fileIdSelection['query']) - if not fileIdSelection['fileIds']: - print(f'No files to {operation} for {user}') - continue - if operation == 'update': - if parameters[DFA_LOCALFILEPATH]: - media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True) - for fileId in fileIdSelection['fileIds']: - if media_body: - result = gapi.call(drive.files(), 'update', - fileId=fileId, convert=parameters[DFA_CONVERT], - ocr=parameters[DFA_OCR], - ocrLanguage=parameters[DFA_OCRLANGUAGE], - media_body=media_body, body=body, fields='id', - supportsAllDrives=True) - print(f'Successfully updated {result["id"]} drive file with content from {parameters[DFA_LOCALFILENAME]}') - else: - result = gapi.call(drive.files(), 'patch', - fileId=fileId, convert=parameters[DFA_CONVERT], - ocr=parameters[DFA_OCR], - ocrLanguage=parameters[DFA_OCRLANGUAGE], body=body, - fields='id', supportsAllDrives=True) - print(f'Successfully updated drive file/folder ID {result["id"]}') - else: - for fileId in fileIdSelection['fileIds']: - result = gapi.call(drive.files(), 'copy', - fileId=fileId, convert=parameters[DFA_CONVERT], - ocr=parameters[DFA_OCR], - ocrLanguage=parameters[DFA_OCRLANGUAGE], - body=body, fields='id', supportsAllDrives=True) - print(f'Successfully copied {fileId} to {result["id"]}') - -def createDriveFile(users): - csv_output = return_id_only = to_drive = False - csv_rows = [] - csv_titles = ['User', 'title', 'id'] - media_body = None - body, parameters = initializeDriveFileAttributes() - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'drivefilename': - body['title'] = sys.argv[i+1] - i += 2 - elif myarg == 'csv': - csv_output = True - i += 1 - elif myarg == 'todrive': - to_drive = True - i += 1 - elif myarg == 'returnidonly': - return_id_only = True - i += 1 - else: - i = getDriveFileAttribute(i, body, parameters, myarg, False) - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - if parameters[DFA_PARENTQUERY]: - more_parents = doDriveSearch(drive, query=parameters[DFA_PARENTQUERY]) - body.setdefault('parents', []) - for a_parent in more_parents: - body['parents'].append({'id': a_parent}) - if parameters[DFA_LOCALFILEPATH]: - media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True) - result = gapi.call(drive.files(), 'insert', - convert=parameters[DFA_CONVERT], ocr=parameters[DFA_OCR], - ocrLanguage=parameters[DFA_OCRLANGUAGE], - media_body=media_body, body=body, fields='id,title,mimeType', - supportsAllDrives=True) - if return_id_only: - sys.stdout.write(f"{result['id']}\n") - elif csv_output: - csv_rows.append({'User': user, 'title': result['title'], 'id': result['id']}) - else: - titleInfo = f'{result["title"]}({result["id"]})' - if parameters[DFA_LOCALFILENAME]: - print(f'Successfully uploaded {parameters[DFA_LOCALFILENAME]} to Drive File {titleInfo}') - else: - created_type = ['Folder', 'File'][result['mimeType'] != MIMETYPE_GA_FOLDER] - print(f'Successfully created Drive {created_type} {titleInfo}') - if csv_output: - display.write_csv_file(csv_rows, csv_titles, 'Files', to_drive) - -HTTP_ERROR_PATTERN = re.compile(r'^.*returned "(.*)">$') - -def downloadDriveFile(users): - i = 5 - fileIdSelection = {'fileIds': [], 'query': None} - csvSheetTitle = revisionId = None - exportFormatName = 'openoffice' - exportFormatChoices = [exportFormatName] - exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName] - targetFolder = GC_Values[GC_DRIVE_DIR] - targetName = None - overwrite = showProgress = targetStdout = False - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'id': - fileIdSelection['fileIds'] = [sys.argv[i+1],] - i += 2 - elif myarg == 'query': - fileIdSelection['query'] = sys.argv[i+1] - i += 2 - elif myarg == 'drivefilename': - fileIdSelection['query'] = f"'me' in owners and title = '{sys.argv[i+1]}'" - i += 2 - elif myarg == 'revision': - revisionId = getInteger(sys.argv[i+1], myarg, minVal=1) - i += 2 - elif myarg == 'csvsheet': - csvSheetTitle = sys.argv[i+1] - csvSheetTitleLower = csvSheetTitle.lower() - i += 2 - elif myarg == 'format': - exportFormatChoices = sys.argv[i+1].replace(',', ' ').lower().split() - exportFormats = [] - for exportFormat in exportFormatChoices: - if exportFormat in DOCUMENT_FORMATS_MAP: - exportFormats.extend(DOCUMENT_FORMATS_MAP[exportFormat]) - else: - controlflow.expected_argument_exit("format", ", ".join(DOCUMENT_FORMATS_MAP), exportFormat) - i += 2 - elif myarg == 'targetfolder': - targetFolder = os.path.expanduser(sys.argv[i+1]) - if not os.path.isdir(targetFolder): - os.makedirs(targetFolder) - i += 2 - elif myarg == 'targetname': - targetName = sys.argv[i+1] - targetStdout = targetName == '-' - i += 2 - elif myarg == 'overwrite': - overwrite = True - i += 1 - elif myarg == 'showprogress': - showProgress = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam get drivefile") - if not fileIdSelection['query'] and not fileIdSelection['fileIds']: - controlflow.system_error_exit(2, 'you need to specify either id, query or drivefilename in order to determine the file(s) to download') - if fileIdSelection['query'] and fileIdSelection['fileIds']: - controlflow.system_error_exit(2, 'you cannot specify multiple file identifiers. Choose one of id, drivefilename, query.') - if csvSheetTitle: - exportFormatName = 'csv' - exportFormatChoices = [exportFormatName] - exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName] - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - if csvSheetTitle: - sheet = buildGAPIServiceObject('sheets', user) - if not sheet: - continue - if fileIdSelection['query']: - fileIdSelection['fileIds'] = doDriveSearch(drive, query=fileIdSelection['query'], quiet=targetStdout) - else: - fileId = fileIdSelection['fileIds'][0] - if fileId[:8].lower() == 'https://' or fileId[:7].lower() == 'http://': - fileIdSelection['fileIds'][0] = getFileIdFromAlternateLink(fileId) - if not fileIdSelection['fileIds']: - print(f'No files to download for {user}') - i = 0 - for fileId in fileIdSelection['fileIds']: - fileExtension = None - result = gapi.call(drive.files(), 'get', - fileId=fileId, fields='fileExtension,fileSize,mimeType,title', supportsAllDrives=True) - fileExtension = result.get('fileExtension') - mimeType = result['mimeType'] - if mimeType == MIMETYPE_GA_FOLDER: - print(f'Skipping download of folder {result["title"]}') - continue - if mimeType in NON_DOWNLOADABLE_MIMETYPES: - print(f'Format of file {result["title"]} not downloadable') - continue - validExtensions = GOOGLEDOC_VALID_EXTENSIONS_MAP.get(mimeType) - if validExtensions: - my_line = 'Downloading Google Doc: %s' - if csvSheetTitle: - my_line += f', Sheet: {csvSheetTitle}' - googleDoc = True - else: - if 'fileSize' in result: - my_line = 'Downloading: %%s of %s bytes' % utils.formatFileSize(int(result['fileSize'])) - else: - my_line = 'Downloading: %s of unknown size' - googleDoc = False - my_line += ' to %s' - csvSheetNotFound = fileDownloaded = fileDownloadFailed = False - for exportFormat in exportFormats: - extension = fileExtension or exportFormat['ext'] - if googleDoc and (extension not in validExtensions): - continue - if targetStdout: - filename = 'stdout' - else: - if targetName: - safe_file_title = targetName - else: - safe_file_title = ''.join(c for c in result['title'] if c in FILENAME_SAFE_CHARS) - if not safe_file_title: - safe_file_title = fileId - filename = os.path.join(targetFolder, safe_file_title) - y = 0 - while True: - if filename.lower()[-len(extension):] != extension.lower(): - filename += extension - if overwrite or not os.path.isfile(filename): - break - y += 1 - filename = os.path.join(targetFolder, f'({y})-{safe_file_title}') - print(my_line % (result['title'], filename)) - spreadsheetUrl = None - if googleDoc: - if csvSheetTitle is None or mimeType != MIMETYPE_GA_SPREADSHEET: - request = drive.files().export_media(fileId=fileId, mimeType=exportFormat['mime']) - if revisionId: - request.uri = f'{request.uri}&revision={revisionId}' - else: - spreadsheet = gapi.call(sheet.spreadsheets(), 'get', - spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))') - for sheet in spreadsheet['sheets']: - if sheet['properties']['title'].lower() == csvSheetTitleLower: - spreadsheetUrl = '{0}?format=csv&id={1}&gid={2}'.format(re.sub('/edit$', '/export', spreadsheet['spreadsheetUrl']), - fileId, sheet['properties']['sheetId']) - break - else: - display.print_error(f'Google Doc: {result["title"]}, Sheet: {csvSheetTitle}, does not exist') - csvSheetNotFound = True - continue - else: - request = drive.files().get_media(fileId=fileId, revisionId=revisionId) - fh = None - try: - fh = open(filename, 'wb') if not targetStdout else sys.stdout - if not spreadsheetUrl: - downloader = googleapiclient.http.MediaIoBaseDownload(fh, request) - done = False - while not done: - status, done = downloader.next_chunk() - if showProgress: - print('Downloaded: {0:>7.2%}'.format(status.progress())) - else: - _, content = drive._http.request(uri=spreadsheetUrl, method='GET') - fh.write(content) - if targetStdout and content[-1] != '\n': - fh.write('\n') - if not targetStdout: - fileutils.close_file(fh) - fileDownloaded = True - break - except (IOError, httplib2.HttpLib2Error) as e: - display.print_error(str(e)) - GM_Globals[GM_SYSEXITRC] = 6 - fileDownloadFailed = True - break - except googleapiclient.http.HttpError as e: - mg = HTTP_ERROR_PATTERN.match(str(e)) - if mg: - display.print_error(mg.group(1)) - else: - display.print_error(str(e)) - fileDownloadFailed = True - break - if fh and not targetStdout: - fileutils.close_file(fh) - os.remove(filename) - if not fileDownloaded and not fileDownloadFailed and not csvSheetNotFound: - display.print_error(f'Format ({",".join(exportFormatChoices)}) not available') - GM_Globals[GM_SYSEXITRC] = 51 - -def showDriveFileInfo(users): - fieldsList = [] - labelsList = [] - fileId = sys.argv[5] - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'allfields': - fieldsList = [] - i += 1 - elif myarg in DRIVEFILE_FIELDS_CHOICES_MAP: - fieldsList.append(DRIVEFILE_FIELDS_CHOICES_MAP[myarg]) - i += 1 - elif myarg in DRIVEFILE_LABEL_CHOICES_MAP: - labelsList.append(DRIVEFILE_LABEL_CHOICES_MAP[myarg]) - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam show fileinfo") - if fieldsList or labelsList: - fieldsList.append('title') - fields = ','.join(set(fieldsList)) - if labelsList: - fields += f',labels({",".join(set(labelsList))})' - else: - fields = '*' - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - feed = gapi.call(drive.files(), 'get', fileId=fileId, fields=fields, supportsAllDrives=True) - if feed: - display.print_json(feed) - -def showDriveFileRevisions(users): - fileId = sys.argv[5] - for user in users: - user, drive = buildDriveGAPIObject(user) - if not drive: - continue - feed = gapi.call(drive.revisions(), 'list', fileId=fileId) - if feed: - display.print_json(feed) - -def transferDriveFiles(users): - target_user = sys.argv[5] - remove_source_user = True - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'keepuser': - remove_source_user = False - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam transfer drive") - target_user, target_drive = buildDriveGAPIObject(target_user) - if not target_drive: - return - target_about = gapi.call(target_drive.about(), 'get', fields='quotaType,quotaBytesTotal,quotaBytesUsed') - if target_about['quotaType'] != 'UNLIMITED': - target_drive_free = int(target_about['quotaBytesTotal']) - int(target_about['quotaBytesUsed']) - else: - target_drive_free = None - for user in users: - user, source_drive = buildDriveGAPIObject(user) - if not source_drive: - continue - counter = 0 - source_about = gapi.call(source_drive.about(), 'get', fields='quotaBytesTotal,quotaBytesUsed,rootFolderId,permissionId') - source_drive_size = int(source_about['quotaBytesUsed']) - if target_drive_free is not None: - if target_drive_free < source_drive_size: - controlflow.system_error_exit(4, MESSAGE_NO_TRANSFER_LACK_OF_DISK_SPACE.format(source_drive_size / 1024 / 1024, target_drive_free / 1024 / 1024)) - print(f'Source drive size: {source_drive_size / 1024 / 1024}mb Target drive free: {target_drive_free / 1024 / 1024}mb') - target_drive_free = target_drive_free - source_drive_size # prep target_drive_free for next user - else: - print(f'Source drive size: {source_drive_size / 1024 / 1024}mb Target drive free: UNLIMITED') - source_root = source_about['rootFolderId'] - source_permissionid = source_about['permissionId'] - print(f'Getting file list for source user: {user}...') - page_message = gapi.got_total_items_msg('Files', '\n') - source_drive_files = gapi.get_all_pages(source_drive.files(), 'list', 'items', page_message=page_message, - q="'me' in owners and trashed = false", fields='items(id,parents,mimeType),nextPageToken') - all_source_file_ids = [] - for source_drive_file in source_drive_files: - all_source_file_ids.append(source_drive_file['id']) - total_count = len(source_drive_files) - print(f'Getting folder list for target user: {target_user}...') - page_message = gapi.got_total_items_msg('Folders', '\n') - target_folders = gapi.get_all_pages(target_drive.files(), 'list', 'items', page_message=page_message, - q="'me' in owners and mimeType = 'application/vnd.google-apps.folder'", fields='items(id,title),nextPageToken') - got_top_folder = False - all_target_folder_ids = [] - for target_folder in target_folders: - all_target_folder_ids.append(target_folder['id']) - if (not got_top_folder) and target_folder['title'] == f'{user} old files': - target_top_folder = target_folder['id'] - got_top_folder = True - if not got_top_folder: - create_folder = gapi.call(target_drive.files(), 'insert', body={'title': f'{user} old files', 'mimeType': 'application/vnd.google-apps.folder'}, fields='id') - target_top_folder = create_folder['id'] - transferred_files = [] - while True: # we loop thru, skipping files until all of their parents are done - skipped_files = False - for drive_file in source_drive_files: - file_id = drive_file['id'] - if file_id in transferred_files: - continue - source_parents = drive_file['parents'] - skip_file_for_now = False - for source_parent in source_parents: - if source_parent['id'] not in all_source_file_ids and source_parent['id'] not in all_target_folder_ids: - continue # means this parent isn't owned by source or target, shouldn't matter - if source_parent['id'] not in transferred_files and source_parent['id'] != source_root: - #print(f'skipping {file_id}') - skipped_files = skip_file_for_now = True - break - if skip_file_for_now: - continue - transferred_files.append(drive_file['id']) - counter += 1 - print(f'Changing owner for file {drive_file["id"]}{currentCount(counter, total_count)}') - body = {'role': 'owner', 'type': 'user', 'value': target_user} - gapi.call(source_drive.permissions(), 'insert', soft_errors=True, fileId=file_id, sendNotificationEmails=False, body=body) - target_parents = [] - for parent in source_parents: - try: - if parent['isRoot']: - target_parents.append({'id': target_top_folder}) - else: - target_parents.append({'id': parent['id']}) - except TypeError: - pass - if not target_parents: - target_parents.append({'id': target_top_folder}) - gapi.call(target_drive.files(), 'patch', soft_errors=True, retry_reasons=[gapi.errors.ErrorReason.NOT_FOUND], fileId=file_id, body={'parents': target_parents}) - if remove_source_user: - gapi.call(target_drive.permissions(), 'delete', soft_errors=True, fileId=file_id, permissionId=source_permissionid) - if not skipped_files: - break - -def sendOrDropEmail(users, method='send'): - body = subject = '' - recipient = labels = sender = None - kwargs = {} - if method in ['insert', 'import']: - kwargs['internalDateSource'] = 'receivedTime' - if method == 'import': - kwargs['neverMarkSpam'] = True - msgHeaders = {} - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'message': - body = sys.argv[i+1] - i += 2 - elif myarg == 'file': - filename = sys.argv[i+1] - i, encoding = getCharSet(i+2) - body = fileutils.read_file(filename, encoding=encoding) - elif myarg == 'subject': - subject = sys.argv[i+1] - i += 2 - elif myarg in ['recipient', 'to']: - recipient = sys.argv[i+1] - i += 2 - elif myarg == 'from': - sender = sys.argv[i+1] - i += 2 - elif myarg == 'header': - msgHeaders[sys.argv[i+1]] = sys.argv[i+2] - i += 3 - elif method in ['insert', 'import'] and myarg == 'labels': - labels = shlexSplitList(sys.argv[i+1]) - i += 2 - elif method in ['insert', 'import'] and myarg == 'deleted': - kwargs['deleted'] = True - i += 1 - elif myarg == 'date': - msgHeaders['Date'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) - if method in ['insert', 'import']: - kwargs['internalDateSource'] = 'dateHeader' - i += 2 - elif method == 'import' and myarg == 'checkspam': - kwargs['neverMarkSpam'] = False - i += 1 - elif method == 'import' and myarg == 'processforcalendar': - kwargs['processForCalendar'] = True - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {method}email") - for user in users: - send_email(subject, body, recipient, sender, user, method, labels, msgHeaders, kwargs) - -def doImap(users): - enable = getBoolean(sys.argv[4], 'gam imap') - body = {'enabled': enable, 'autoExpunge': True, 'expungeBehavior': 'archive', 'maxFolderSize': 0} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'noautoexpunge': - body['autoExpunge'] = False - i += 1 - elif myarg == 'expungebehavior': - opt = sys.argv[i+1].lower() - if opt in EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP: - body['expungeBehavior'] = EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP[opt] - i += 2 - else: - controlflow.expected_argument_exit("gam imap expungebehavior", ", ".join(EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP), opt) - elif myarg == 'maxfoldersize': - opt = sys.argv[i+1].lower() - if opt in EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES: - body['maxFolderSize'] = int(opt) - i += 2 - else: - controlflow.expected_argument_exit("gam imap maxfoldersize", "| ".join(EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES), opt) - else: - controlflow.invalid_argument_exit(myarg, "gam imap") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Setting IMAP Access to {str(enable)} for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings(), 'updateImap', - soft_errors=True, - userId='me', body=body) - -def doLanguage(users): - i = 0 - count = len(users) - displayLanguage = sys.argv[4] - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Setting languaged to {displayLanguage} for {user}{currentCount(i, count)}') - result = gapi.call(gmail.users().settings(), 'updateLanguage', userId='me', body={'displayLanguage': displayLanguage}) - print(f'Language is set to {result["displayLanguage"]} for {user}') - -def getLanguage(users): - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings(), 'getLanguage', - soft_errors=True, - userId='me') - if result: - print(f'User: {user}, Language: {result["displayLanguage"]}{currentCount(i, count)}') - -def getImap(users): - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings(), 'getImap', - soft_errors=True, - userId='me') - if result: - enabled = result['enabled'] - if enabled: - print(f'User: {user}, IMAP Enabled: {enabled}, autoExpunge: {result["autoExpunge"]}, expungeBehavior: {result["expungeBehavior"]}, maxFolderSize: {result["maxFolderSize"]}{currentCount(i, count)}') - else: - print(f'User: {user}, IMAP Enabled: {enabled}{currentCount(i, count)}') - -def getProductAndSKU(sku): - l_sku = sku.lower().replace('-', '').replace(' ', '') - for a_sku, sku_values in list(SKUS.items()): - if l_sku == a_sku.lower().replace('-', '') or l_sku in sku_values['aliases'] or l_sku == sku_values['displayName'].lower().replace(' ', ''): - return (sku_values['product'], a_sku) - try: - product = re.search('^([A-Z,a-z]*-[A-Z,a-z]*)', sku).group(1) - except AttributeError: - product = sku - return (product, sku) - -def doLicense(users, operation): - lic = buildGAPIObject('licensing') - sku = sys.argv[5] - productId, skuId = getProductAndSKU(sku) - i = 6 - if len(sys.argv) > 6 and sys.argv[i].lower() in ['product', 'productid']: - productId = sys.argv[i+1] - i += 2 - for user in users: - if operation == 'delete': - print(f'Removing license {_formatSKUIdDisplayName(skuId)} from user {user}') - gapi.call(lic.licenseAssignments(), operation, soft_errors=True, productId=productId, skuId=skuId, userId=user) - elif operation == 'insert': - print(f'Adding license {_formatSKUIdDisplayName(skuId)} to user {user}') - gapi.call(lic.licenseAssignments(), operation, soft_errors=True, productId=productId, skuId=skuId, body={'userId': user}) - elif operation == 'patch': - try: - old_sku = sys.argv[i] - if old_sku.lower() == 'from': - old_sku = sys.argv[i+1] - except KeyError: - controlflow.system_error_exit(2, 'You need to specify the user\'s old SKU as the last argument') - _, old_sku = getProductAndSKU(old_sku) - print(f'Changing user {user} from license {_formatSKUIdDisplayName(old_sku)} to {_formatSKUIdDisplayName(skuId)}') - gapi.call(lic.licenseAssignments(), operation, soft_errors=True, productId=productId, skuId=old_sku, userId=user, body={'skuId': skuId}) - -def doPop(users): - enable = getBoolean(sys.argv[4], 'gam pop') - body = {'accessWindow': ['disabled', 'allMail'][enable], 'disposition': 'leaveInInbox'} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'for': - opt = sys.argv[i+1].lower() - if opt in EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP: - body['accessWindow'] = EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP[opt] - i += 2 - else: - controlflow.expected_argument_exit("gam pop for", ", ".join(EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP), opt) - elif myarg == 'action': - opt = sys.argv[i+1].lower() - if opt in EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP: - body['disposition'] = EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP[opt] - i += 2 - else: - controlflow.expected_argument_exit("gam pop action", ", ".join(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP), opt) - elif myarg == 'confirm': - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam pop") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Setting POP Access to {str(enable)} for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings(), 'updatePop', - soft_errors=True, - userId='me', body=body) - -def getPop(users): - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings(), 'getPop', - soft_errors=True, - userId='me') - if result: - enabled = result['accessWindow'] != 'disabled' - if enabled: - print(f'User: {user}, POP Enabled: {enabled}, For: {result["accessWindow"]}, Action: {result["disposition"]}{currentCount(i, count)}') - else: - print(f'User: {user}, POP Enabled: {enabled}{currentCount(i, count)}') - -SMTPMSA_DISPLAY_FIELDS = ['host', 'port', 'securityMode'] - -def _showSendAs(result, j, jcount, formatSig): - if result['displayName']: - print(f'SendAs Address: {result["displayName"]} <{result["sendAsEmail"]}>{currentCount(j, jcount)}') - else: - print(f'SendAs Address: <{result["sendAsEmail"]}>{currentCount(j, jcount)}') - if result.get('replyToAddress'): - print(f' ReplyTo: {result["replyToAddress"]}') - print(f' IsPrimary: {result.get("isPrimary", False)}') - print(f' Default: {result.get("isDefault", False)}') - if not result.get('isPrimary', False): - print(f' TreatAsAlias: {result.get("treatAsAlias", False)}') - if 'smtpMsa' in result: - for field in SMTPMSA_DISPLAY_FIELDS: - if field in result['smtpMsa']: - print(f' smtpMsa.{field}: {result["smtpMsa"][field]}') - if 'verificationStatus' in result: - print(f' Verification Status: {result["verificationStatus"]}') - sys.stdout.write(' Signature:\n ') - signature = result.get('signature') - if not signature: - signature = 'None' - if formatSig: - print(utils.indentMultiLineText(utils.dehtml(signature), n=4)) - else: - print(utils.indentMultiLineText(signature, n=4)) - -def _processTags(tagReplacements, message): - while True: - match = RT_PATTERN.search(message) - if not match: - break - if tagReplacements.get(match.group(1)): - message = RT_OPEN_PATTERN.sub('', message, count=1) - message = RT_CLOSE_PATTERN.sub('', message, count=1) - else: - message = RT_STRIP_PATTERN.sub('', message, count=1) - while True: - match = RT_TAG_REPLACE_PATTERN.search(message) - if not match: - break - message = re.sub(match.group(0), tagReplacements.get(match.group(1), ''), message) - return message - -def _processSignature(tagReplacements, signature, html): - if signature: - signature = signature.replace('\r', '').replace('\\n', '
') - if tagReplacements: - signature = _processTags(tagReplacements, signature) - if not html: - signature = signature.replace('\n', '
') - return signature - -def getSendAsAttributes(i, myarg, body, tagReplacements, command): - if myarg == 'replace': - matchTag = utils.get_string(i+1, 'Tag') - matchReplacement = utils.get_string(i+2, 'String', minLen=0) - tagReplacements[matchTag] = matchReplacement - i += 3 - elif myarg == 'name': - body['displayName'] = sys.argv[i+1] - i += 2 - elif myarg == 'replyto': - body['replyToAddress'] = sys.argv[i+1] - i += 2 - elif myarg == 'default': - body['isDefault'] = True - i += 1 - elif myarg == 'treatasalias': - body['treatAsAlias'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - else: - controlflow.invalid_argument_exit(myarg, f"gam {command}") - return i - -SMTPMSA_PORTS = ['25', '465', '587'] -SMTPMSA_SECURITY_MODES = ['none', 'ssl', 'starttls'] -SMTPMSA_REQUIRED_FIELDS = ['host', 'port', 'username', 'password'] - -def addUpdateSendAs(users, i, addCmd): - emailAddress = normalizeEmailAddressOrUID(sys.argv[i], noUid=True) - i += 1 - if addCmd: - command = ['sendas', 'add sendas'][i == 6] - body = {'sendAsEmail': emailAddress, 'displayName': sys.argv[i]} - i += 1 - else: - command = 'update sendas' - body = {} - signature = None - smtpMsa = {} - tagReplacements = {} - html = False - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['signature', 'sig']: - signature = sys.argv[i+1] - i += 2 - if signature.lower() == 'file': - filename = sys.argv[i] - i, encoding = getCharSet(i+1) - signature = fileutils.read_file(filename, encoding=encoding) - elif myarg == 'html': - html = True - i += 1 - elif addCmd and myarg.startswith('smtpmsa.'): - if myarg == 'smtpmsa.host': - smtpMsa['host'] = sys.argv[i+1] - i += 2 - elif myarg == 'smtpmsa.port': - value = sys.argv[i+1].lower() - if value not in SMTPMSA_PORTS: - controlflow.expected_argument_exit(myarg, ", ".join(SMTPMSA_PORTS), value) - smtpMsa['port'] = int(value) - i += 2 - elif myarg == 'smtpmsa.username': - smtpMsa['username'] = sys.argv[i+1] - i += 2 - elif myarg == 'smtpmsa.password': - smtpMsa['password'] = sys.argv[i+1] - i += 2 - elif myarg == 'smtpmsa.securitymode': - value = sys.argv[i+1].lower() - if value not in SMTPMSA_SECURITY_MODES: - controlflow.expected_argument_exit(myarg, ", ".join(SMTPMSA_SECURITY_MODES), value) - smtpMsa['securityMode'] = value - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {command}") - else: - i = getSendAsAttributes(i, myarg, body, tagReplacements, command) - if signature is not None: - body['signature'] = _processSignature(tagReplacements, signature, html) - if smtpMsa: - for field in SMTPMSA_REQUIRED_FIELDS: - if field not in smtpMsa: - controlflow.system_error_exit(2, f'smtpmsa.{field} is required.') - body['smtpMsa'] = smtpMsa - kwargs = {'body': body} - if not addCmd: - kwargs['sendAsEmail'] = emailAddress - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Allowing {user} to send as {emailAddress}{currentCount(i, count)}') - gapi.call(gmail.users().settings().sendAs(), ['patch', 'create'][addCmd], - soft_errors=True, - userId='me', **kwargs) - -def deleteSendAs(users): - emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Disallowing {user} to send as {emailAddress}{currentCount(i, count)}') - gapi.call(gmail.users().settings().sendAs(), 'delete', - soft_errors=True, - userId='me', sendAsEmail=emailAddress) - -def updateSmime(users): - smimeIdBase = None - sendAsEmailBase = None - make_default = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'id': - smimeIdBase = sys.argv[i+1] - i += 2 - elif myarg in ['sendas', 'sendasemail']: - sendAsEmailBase = sys.argv[i+1] - i += 2 - elif myarg in ['default']: - make_default = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam update smime") - if not make_default: - print('Nothing to update for smime.') - sys.exit(0) - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - sendAsEmail = sendAsEmailBase if sendAsEmailBase else user - if not smimeIdBase: - result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'list', userId='me', sendAsEmail=sendAsEmail, fields='smimeInfo(id)') - smimes = result.get('smimeInfo', []) - if not smimes: - controlflow.system_error_exit(3, f'{user} has no S/MIME certificates for sendas address {sendAsEmail}') - if len(smimes) > 1: - certList = "\n ".join([smime["id"] for smime in smimes]) - controlflow.system_error_exit(3, f'{user} has more than one S/MIME certificate. Please specify a cert to update:\n {certList}') - smimeId = smimes[0]['id'] - else: - smimeId = smimeIdBase - print(f'Setting smime id {smimeId} as default for user {user} and sendas {sendAsEmail}') - gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'setDefault', userId='me', sendAsEmail=sendAsEmail, id=smimeId) - -def deleteSmime(users): - smimeIdBase = None - sendAsEmailBase = None - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'id': - smimeIdBase = sys.argv[i+1] - i += 2 - elif myarg in ['sendas', 'sendasemail']: - sendAsEmailBase = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(myarg, "gam delete smime") - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - sendAsEmail = sendAsEmailBase if sendAsEmailBase else user - if not smimeIdBase: - result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'list', userId='me', sendAsEmail=sendAsEmail, fields='smimeInfo(id)') - smimes = result.get('smimeInfo', []) - if not smimes: - controlflow.system_error_exit(3, f'{user} has no S/MIME certificates for sendas address {sendAsEmail}') - if len(smimes) > 1: - certList = "\n ".join([smime["id"] for smime in smimes]) - controlflow.system_error_exit(3, f'{user} has more than one S/MIME certificate. Please specify a cert to delete:\n {certList}') - smimeId = smimes[0]['id'] - else: - smimeId = smimeIdBase - print(f'Deleting smime id {smimeId} for user {user} and sendas {sendAsEmail}') - gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'delete', userId='me', sendAsEmail=sendAsEmail, id=smimeId) - -def printShowSmime(users, csvFormat): - if csvFormat: - todrive = False - titles = ['User'] - csvRows = [] - primaryonly = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'primaryonly': - primaryonly = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} smime") - i = 0 - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - if primaryonly: - sendAsEmails = [user] - else: - result = gapi.call(gmail.users().settings().sendAs(), 'list', userId='me', fields='sendAs(sendAsEmail)') - sendAsEmails = [] - for sendAs in result['sendAs']: - sendAsEmails.append(sendAs['sendAsEmail']) - for sendAsEmail in sendAsEmails: - result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'list', sendAsEmail=sendAsEmail, userId='me') - smimes = result.get('smimeInfo', []) - for j, _ in enumerate(smimes): - smimes[j]['expiration'] = utils.formatTimestampYMDHMS(smimes[j]['expiration']) - if csvFormat: - for smime in smimes: - display.add_row_titles_to_csv_file(utils.flatten_json(smime, flattened={'User': user}), csvRows, titles) - else: - display.print_json(smimes) - if csvFormat: - display.write_csv_file(csvRows, titles, 'S/MIME', todrive) - -def printShowSendAs(users, csvFormat): - if csvFormat: - todrive = False - titles = ['User', 'displayName', 'sendAsEmail', 'replyToAddress', 'isPrimary', 'isDefault', 'treatAsAlias', 'verificationStatus'] - csvRows = [] - formatSig = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - elif not csvFormat and myarg == 'format': - formatSig = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} sendas") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings().sendAs(), 'list', - soft_errors=True, - userId='me') - jcount = len(result.get('sendAs', [])) if (result) else 0 - if not csvFormat: - print(f'User: {user}, SendAs Addresses:{currentCount(i, count)}') - if jcount == 0: - continue - j = 0 - for sendas in result['sendAs']: - j += 1 - _showSendAs(sendas, j, jcount, formatSig) - else: - if jcount == 0: - continue - for sendas in result['sendAs']: - row = {'User': user, 'isPrimary': False} - for item in sendas: - if item != 'smtpMsa': - if item not in titles: - titles.append(item) - row[item] = sendas[item] - else: - for field in SMTPMSA_DISPLAY_FIELDS: - if field in sendas[item]: - title = f'smtpMsa.{field}' - if title not in titles: - titles.append(title) - row[title] = sendas[item][field] - csvRows.append(row) - if csvFormat: - display.write_csv_file(csvRows, titles, 'SendAs', todrive) - -def infoSendAs(users): - emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) - formatSig = False - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'format': - formatSig = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam info sendas") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'User: {user}, Show SendAs Address:{currentCount(i, count)}') - result = gapi.call(gmail.users().settings().sendAs(), 'get', - soft_errors=True, - userId='me', sendAsEmail=emailAddress) - if result: - _showSendAs(result, i, count, formatSig) - -def addSmime(users): - sendAsEmailBase = None - setDefault = False - body = {} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'file': - smimefile = sys.argv[i+1] - smimeData = fileutils.read_file(smimefile, mode='rb') - body['pkcs12'] = base64.urlsafe_b64encode(smimeData).decode(UTF8) - i += 2 - elif myarg == 'password': - body['encryptedKeyPassword'] = sys.argv[i+1] - i += 2 - elif myarg == 'default': - setDefault = True - i += 1 - elif myarg in ['sendas', 'sendasemail']: - sendAsEmailBase = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(myarg, "gam add smime") - if 'pkcs12' not in body: - controlflow.system_error_exit(3, 'you must specify a file to upload') - i = 0 - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - sendAsEmail = sendAsEmailBase if sendAsEmailBase else user - result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'insert', userId='me', sendAsEmail=sendAsEmail, body=body) - if setDefault: - gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'setDefault', userId='me', sendAsEmail=sendAsEmail, id=result['id']) - print(f'Added S/MIME certificate for user {user} sendas {sendAsEmail} issued by {result["issuerCn"]}') - -def getLabelAttributes(i, myarg, body, function): - if myarg == 'labellistvisibility': - value = sys.argv[i+1].lower().replace('_', '') - if value == 'hide': - body['labelListVisibility'] = 'labelHide' - elif value == 'show': - body['labelListVisibility'] = 'labelShow' - elif value == 'showifunread': - body['labelListVisibility'] = 'labelShowIfUnread' - else: - controlflow.expected_argument_exit("label_list_visibility", ", ".join(["hide", "show", "show_if_unread"]), value) - i += 2 - elif myarg == 'messagelistvisibility': - value = sys.argv[i+1].lower().replace('_', '') - if value not in ['hide', 'show']: - controlflow.expected_argument_exit("message_list_visibility", ", ".join(["hide", "show"]), value) - body['messageListVisibility'] = value - i += 2 - elif myarg == 'backgroundcolor': - body.setdefault('color', {}) - body['color']['backgroundColor'] = getLabelColor(sys.argv[i+1]) - i += 2 - elif myarg == 'textcolor': - body.setdefault('color', {}) - body['color']['textColor'] = getLabelColor(sys.argv[i+1]) - i += 2 - else: - controlflow.invalid_argument_exit(myarg, f"gam {function} labels") - return i - -def checkLabelColor(body): - if 'color' not in body: - return - if 'backgroundColor' in body['color']: - if 'textColor' in body['color']: - return - controlflow.system_error_exit(2, 'textcolor is required.') - controlflow.system_error_exit(2, 'backgroundcolor is required.') - -def doLabel(users, i): - label = sys.argv[i] - i += 1 - body = {'name': label} - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - i = getLabelAttributes(i, myarg, body, "create") - checkLabelColor(body) - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Creating label {label} for {user}{currentCount(i, count)}') - gapi.call(gmail.users().labels(), 'create', soft_errors=True, userId=user, body=body) - -PROCESS_MESSAGE_FUNCTION_TO_ACTION_MAP = {'delete': 'deleted', 'trash': 'trashed', 'untrash': 'untrashed', 'modify': 'modified'} - -def labelsToLabelIds(gmail, labels): - allLabels = { - 'INBOX': 'INBOX', 'SPAM': 'SPAM', 'TRASH': 'TRASH', - 'UNREAD': 'UNREAD', 'STARRED': 'STARRED', 'IMPORTANT': 'IMPORTANT', - 'SENT': 'SENT', 'DRAFT': 'DRAFT', - 'CATEGORY_PERSONAL': 'CATEGORY_PERSONAL', - 'CATEGORY_SOCIAL': 'CATEGORY_SOCIAL', - 'CATEGORY_PROMOTIONS': 'CATEGORY_PROMOTIONS', - 'CATEGORY_UPDATES': 'CATEGORY_UPDATES', - 'CATEGORY_FORUMS': 'CATEGORY_FORUMS', - } - labelIds = list() - for label in labels: - if label not in allLabels: - # first refresh labels in user mailbox - label_results = gapi.call(gmail.users().labels(), 'list', - userId='me', fields='labels(id,name,type)') - for a_label in label_results['labels']: - if a_label['type'] == 'system': - allLabels[a_label['id']] = a_label['id'] - else: - allLabels[a_label['name']] = a_label['id'] - if label not in allLabels: - # if still not there, create it - label_results = gapi.call(gmail.users().labels(), 'create', - body={'labelListVisibility': 'labelShow', - 'messageListVisibility': 'show', 'name': label}, - userId='me', fields='id') - allLabels[label] = label_results['id'] - try: - labelIds.append(allLabels[label]) - except KeyError: - pass - if label.find('/') != -1: - # make sure to create parent labels for proper nesting - parent_label = label[:label.rfind('/')] - while True: - if not parent_label in allLabels: - label_result = gapi.call(gmail.users().labels(), 'create', - userId='me', body={'name': parent_label}) - allLabels[parent_label] = label_result['id'] - if parent_label.find('/') == -1: - break - parent_label = parent_label[:parent_label.rfind('/')] - return labelIds - -def doProcessMessagesOrThreads(users, function, unit='messages'): - query = None - doIt = False - maxToProcess = 1 - body = {} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'query': - query = sys.argv[i+1] - i += 2 - elif myarg == 'doit': - doIt = True - i += 1 - elif myarg in ['maxtodelete', 'maxtotrash', 'maxtomodify', 'maxtountrash']: - maxToProcess = getInteger(sys.argv[i+1], myarg, minVal=0) - i += 2 - elif (function == 'modify') and (myarg == 'addlabel'): - body.setdefault('addLabelIds', []) - body['addLabelIds'].append(sys.argv[i+1]) - i += 2 - elif (function == 'modify') and (myarg == 'removelabel'): - body.setdefault('removeLabelIds', []) - body['removeLabelIds'].append(sys.argv[i+1]) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {function} {unit}") - if not query: - controlflow.system_error_exit(2, 'No query specified. You must specify some query!') - action = PROCESS_MESSAGE_FUNCTION_TO_ACTION_MAP[function] - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Searching {unit.capitalize()} for {user}') - unitmethod = getattr(gmail.users(), unit) - page_message = gapi.got_total_items_msg(f'{unit.capitalize()} for user {user}', '') - listResult = gapi.get_all_pages(unitmethod(), 'list', unit, page_message=page_message, - userId='me', q=query, includeSpamTrash=True, soft_errors=True, fields=f'nextPageToken,{unit}(id)') - result_count = len(listResult) - if not doIt or result_count == 0: - print(f'would try to {function} {result_count} messages for user {user} (max {maxToProcess})\n') - continue - if result_count > maxToProcess: - print(f'WARNING: refusing to {function} ANY messages for user {user} since max messages to process is {maxToProcess} and messages to be {action} is {result_count}\n') - continue - kwargs = {'body': {}} - for my_key in body: - kwargs['body'][my_key] = labelsToLabelIds(gmail, body[my_key]) - i = 0 - if unit == 'messages' and function in ['delete', 'modify']: - batchFunction = f'batch{function.title()}' - id_batches = [[]] - for a_unit in listResult: - id_batches[i].append(a_unit['id']) - if len(id_batches[i]) == 1000: - i += 1 - id_batches.append([]) - processed_messages = 0 - for id_batch in id_batches: - kwargs['body']['ids'] = id_batch - print(f'{function} {len(id_batch)} messages') - gapi.call(unitmethod(), batchFunction, - userId='me', **kwargs) - processed_messages += len(id_batch) - print(f'{function} {processed_messages} of {result_count} messages') - continue - if not kwargs['body']: - del kwargs['body'] - for a_unit in listResult: - i += 1 - print(f' {function} {unit} {a_unit["id"]} for user {user}{currentCount(i, result_count)}') - gapi.call(unitmethod(), function, - id=a_unit['id'], userId='me', **kwargs) - -def doDeleteLabel(users): - label = sys.argv[5] - label_name_lower = label.lower() - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Getting all labels for {user}...') - labels = gapi.call(gmail.users().labels(), 'list', userId=user, fields='labels(id,name,type)') - del_labels = [] - if label == '--ALL_LABELS--': - for del_label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True): - if del_label['type'] != 'system': - del_labels.append(del_label) - elif label[:6].lower() == 'regex:': - regex = label[6:] - p = re.compile(regex) - for del_label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True): - if del_label['type'] != 'system' and p.match(del_label['name']): - del_labels.append(del_label) - else: - for del_label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True): - if label_name_lower == del_label['name'].lower(): - del_labels.append(del_label) - break - else: - print(f' Error: no such label for {user}') - continue - bcount = 0 - i = 0 - count = len(del_labels) - dbatch = gmail.new_batch_http_request(callback=gmail_del_result) - for del_me in del_labels: - i += 1 - print(f' deleting label {del_me["name"]}{currentCount(i, count)}') - dbatch.add(gmail.users().labels().delete(userId=user, id=del_me['id'])) - bcount += 1 - if bcount == 10: - dbatch.execute() - dbatch = gmail.new_batch_http_request(callback=gmail_del_result) - bcount = 0 - if bcount > 0: - dbatch.execute() - -def gmail_del_result(request_id, response, exception): - if exception: - print(exception) - -def showLabels(users): - i = 5 - onlyUser = showCounts = False - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'onlyuser': - onlyUser = True - i += 1 - elif myarg == 'showcounts': - showCounts = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show labels") - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - labels = gapi.call(gmail.users().labels(), 'list', userId=user, soft_errors=True) - if labels: - for label in labels['labels']: - if onlyUser and (label['type'] == 'system'): - continue - print(label['name']) - for a_key in label: - if a_key == 'name': - continue - print(f' {a_key}: {label[a_key]}') - if showCounts: - counts = gapi.call(gmail.users().labels(), 'get', - userId=user, id=label['id'], - fields='messagesTotal,messagesUnread,threadsTotal,threadsUnread') - for a_key in counts: - print(f' {a_key}: {counts[a_key]}') - print('') - -def showGmailProfile(users): - todrive = False - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show gmailprofile") - csvRows = [] - titles = ['emailAddress'] - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - sys.stderr.write(f'Getting Gmail profile for {user}\n') - try: - results = gapi.call(gmail.users(), 'getProfile', - throw_reasons=gapi.errors.GMAIL_THROW_REASONS, - userId='me') - if results: - for item in results: - if item not in titles: - titles.append(item) - csvRows.append(results) - except gapi.errors.GapiServiceNotAvailableError: - entityServiceNotApplicableWarning('User', user, i, count) - display.sort_csv_titles(['emailAddress',], titles) - display.write_csv_file(csvRows, titles, list_type='Gmail Profiles', todrive=todrive) - -def updateLabels(users): - label_name = sys.argv[5] - label_name_lower = label_name.lower() - body = {} - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'name': - body['name'] = sys.argv[i+1] - i += 2 - else: - i = getLabelAttributes(i, myarg, body, "update") - checkLabelColor(body) - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - labels = gapi.call(gmail.users().labels(), 'list', userId=user, fields='labels(id,name)') - for label in labels['labels']: - if label['name'].lower() == label_name_lower: - gapi.call(gmail.users().labels(), 'patch', soft_errors=True, - userId=user, id=label['id'], body=body) - break - else: - print(f'Error: user does not have a label named {label_name}') - -def cleanLabelQuery(labelQuery): - for ch in '/ (){}': - labelQuery = labelQuery.replace(ch, '-') - return labelQuery.lower() - -def renameLabels(users): - search = '^Inbox/(.*)$' - replace = '%s' - merge = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'search': - search = sys.argv[i+1] - i += 2 - elif myarg == 'replace': - replace = sys.argv[i+1] - i += 2 - elif myarg == 'merge': - merge = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam rename label") - pattern = re.compile(search, re.IGNORECASE) - for user in users: - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - labels = gapi.call(gmail.users().labels(), 'list', userId=user) - print(f'got {len(labels["labels"])} labels') - for label in labels['labels']: - if label['type'] == 'system': - continue - match_result = re.search(pattern, label['name']) - if match_result is not None: - try: - new_label_name = replace % match_result.groups() - except TypeError: - controlflow.system_error_exit(2, f'The number of subfields ({len(match_result.groups())}) in search "{search}" does not match the number of subfields ({replace.count("%s")}) in replace "{replace}"') - print(f' Renaming "{label["name"]}" to "{new_label_name}"') - try: - gapi.call(gmail.users().labels(), 'patch', soft_errors=True, throw_reasons=[gapi.errors.ErrorReason.ABORTED], id=label['id'], userId=user, body={'name': new_label_name}) - except gapi.errors.GapiAbortedError: - if merge: - print(f' Merging {label["name"]} label to existing {new_label_name} label') - messages_to_relabel = gapi.get_all_pages(gmail.users().messages(), 'list', 'messages', - userId=user, q=f'label:{cleanLabelQuery(label["name"])}') - if messages_to_relabel: - for new_label in labels['labels']: - if new_label['name'].lower() == new_label_name.lower(): - new_label_id = new_label['id'] - body = {'addLabelIds': [new_label_id]} - break - j = 0 - jcount = len(messages_to_relabel) - for message_to_relabel in messages_to_relabel: - j += 1 - print(f' relabeling message {message_to_relabel["id"]}{currentCount(j, jcount)}') - gapi.call(gmail.users().messages(), 'modify', userId=user, id=message_to_relabel['id'], body=body) - else: - print(f' no messages with {label["name"]} label') - print(f' Deleting label {label["name"]}') - gapi.call(gmail.users().labels(), 'delete', id=label['id'], userId=user) - else: - print(f' Error: looks like {new_label_name} already exists, not renaming. Use the "merge" argument to merge the labels') - -def _getUserGmailLabels(gmail, user, i, count, **kwargs): - try: - labels = gapi.call(gmail.users().labels(), 'list', - throw_reasons=gapi.errors.GMAIL_THROW_REASONS, - userId='me', **kwargs) - if not labels: - labels = {'labels': []} - return labels - except gapi.errors.GapiServiceNotAvailableError: - entityServiceNotApplicableWarning('User', user, i, count) - return None - -def _getLabelId(labels, labelName): - for label in labels['labels']: - if labelName in (label['id'], label['name']): - return label['id'] - return None - -def _getLabelName(labels, labelId): - for label in labels['labels']: - if label['id'] == labelId: - return label['name'] - return labelId - -def _printFilter(user, userFilter, labels): - row = {'User': user, 'id': userFilter['id']} - if 'criteria' in userFilter: - for item in userFilter['criteria']: - if item in ['hasAttachment', 'excludeChats']: - row[item] = item - elif item == 'size': - row[item] = f'size {userFilter["criteria"]["sizeComparison"]} {userFilter["criteria"][item]}' - elif item == 'sizeComparison': - pass - else: - row[item] = f'{item} {userFilter["criteria"][item]}' - else: - row['error'] = 'NoCriteria' - if 'action' in userFilter: - for labelId in userFilter['action'].get('addLabelIds', []): - if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP: - row[FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]] = FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId] - else: - row['label'] = f'label {_getLabelName(labels, labelId)}' - for labelId in userFilter['action'].get('removeLabelIds', []): - if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP: - row[FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]] = FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId] - if userFilter['action'].get('forward'): - row['forward'] = f'forward {userFilter["action"]["forward"]}' - else: - row['error'] = 'NoActions' - return row - -def _showFilter(userFilter, j, jcount, labels): - print(f' Filter: {userFilter["id"]}{currentCount(j, jcount)}') - print(' Criteria:') - if 'criteria' in userFilter: - for item in userFilter['criteria']: - if item in ['hasAttachment', 'excludeChats']: - print(f' {item}') - elif item == 'size': - print(f' {item} {userFilter["criteria"]["sizeComparison"]} {userFilter["criteria"][item]}') - elif item == 'sizeComparison': - pass - else: - print(f' {item} "{userFilter["criteria"][item]}"') - else: - print(' ERROR: No Filter criteria') - print(' Actions:') - if 'action' in userFilter: - for labelId in userFilter['action'].get('addLabelIds', []): - if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP: - print(f' {FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]}') - else: - print(f' label "{_getLabelName(labels, labelId)}"') - for labelId in userFilter['action'].get('removeLabelIds', []): - if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP: - print(f' {FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]}') - if userFilter['action'].get('forward'): - print(f' Forwarding Address: {userFilter["action"]["forward"]}') - else: - print(' ERROR: No Filter actions') - -def addFilter(users, i): - body = {} - addLabelName = None - addLabelIds = [] - removeLabelIds = [] - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in FILTER_CRITERIA_CHOICES_MAP: - myarg = FILTER_CRITERIA_CHOICES_MAP[myarg] - body.setdefault('criteria', {}) - if myarg == 'from': - body['criteria'][myarg] = sys.argv[i+1] - i += 2 - elif myarg == 'to': - body['criteria'][myarg] = sys.argv[i+1] - i += 2 - elif myarg in ['subject', 'query', 'negatedQuery']: - body['criteria'][myarg] = sys.argv[i+1] - i += 2 - elif myarg in ['hasAttachment', 'excludeChats']: - body['criteria'][myarg] = True - i += 1 - elif myarg == 'size': - body['criteria']['sizeComparison'] = sys.argv[i+1].lower() - if body['criteria']['sizeComparison'] not in ['larger', 'smaller']: - controlflow.system_error_exit(2, f'size must be followed by larger or smaller; got {sys.argv[i+1].lower()}') - body['criteria'][myarg] = sys.argv[i+2] - i += 3 - elif myarg in FILTER_ACTION_CHOICES: - body.setdefault('action', {}) - if myarg == 'label': - addLabelName = sys.argv[i+1] - i += 2 - elif myarg == 'important': - addLabelIds.append('IMPORTANT') - if 'IMPORTANT' in removeLabelIds: - removeLabelIds.remove('IMPORTANT') - i += 1 - elif myarg == 'star': - addLabelIds.append('STARRED') - i += 1 - elif myarg == 'trash': - addLabelIds.append('TRASH') - i += 1 - elif myarg == 'notimportant': - removeLabelIds.append('IMPORTANT') - if 'IMPORTANT' in addLabelIds: - addLabelIds.remove('IMPORTANT') - i += 1 - elif myarg == 'markread': - removeLabelIds.append('UNREAD') - i += 1 - elif myarg == 'archive': - removeLabelIds.append('INBOX') - i += 1 - elif myarg == 'neverspam': - removeLabelIds.append('SPAM') - i += 1 - elif myarg == 'forward': - body['action']['forward'] = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam filter") - if 'criteria' not in body: - controlflow.system_error_exit(2, f'you must specify a crtieria <{"|".join(FILTER_CRITERIA_CHOICES_MAP)}> for "gam filter"') - if 'action' not in body: - controlflow.system_error_exit(2, f'you must specify an action <{"|".join(FILTER_ACTION_CHOICES)}> for "gam filter"') - if removeLabelIds: - body['action']['removeLabelIds'] = removeLabelIds - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - labels = _getUserGmailLabels(gmail, user, i, count, fields='labels(id,name)') - if not labels: - continue - if addLabelIds: - body['action']['addLabelIds'] = addLabelIds[:] - if addLabelName: - if not addLabelIds: - body['action']['addLabelIds'] = [] - addLabelId = _getLabelId(labels, addLabelName) - if not addLabelId: - result = gapi.call(gmail.users().labels(), 'create', - soft_errors=True, - userId='me', body={'name': addLabelName}, fields='id') - if not result: - continue - addLabelId = result['id'] - body['action']['addLabelIds'].append(addLabelId) - print(f'Adding filter for {user}{currentCount(i, count)}') - result = gapi.call(gmail.users().settings().filters(), 'create', - soft_errors=True, - userId='me', body=body) - if result: - print(f'User: {user}, Filter: {result["id"]}, Added{currentCount(i, count)}') - -def deleteFilters(users): - filterId = sys.argv[5] - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Deleting filter {filterId} for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings().filters(), 'delete', - soft_errors=True, - userId='me', id=filterId) - -def printShowFilters(users, csvFormat): - if csvFormat: - todrive = False - csvRows = [] - titles = ['User', 'id'] - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} filter") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - labels = gapi.call(gmail.users().labels(), 'list', - soft_errors=True, - userId='me', fields='labels(id,name)') - if not labels: - labels = {'labels': []} - result = gapi.call(gmail.users().settings().filters(), 'list', - soft_errors=True, - userId='me') - jcount = len(result.get('filter', [])) if (result) else 0 - if not csvFormat: - print(f'User: {user}, Filters:{currentCount(i, count)}') - if jcount == 0: - continue - j = 0 - for userFilter in result['filter']: - j += 1 - _showFilter(userFilter, j, jcount, labels) - else: - if jcount == 0: - continue - for userFilter in result['filter']: - row = _printFilter(user, userFilter, labels) - for item in row: - if item not in titles: - titles.append(item) - csvRows.append(row) - if csvFormat: - display.sort_csv_titles(['User', 'id'], titles) - display.write_csv_file(csvRows, titles, 'Filters', todrive) - -def infoFilters(users): - filterId = sys.argv[5] - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - labels = gapi.call(gmail.users().labels(), 'list', - soft_errors=True, - userId='me', fields='labels(id,name)') - if not labels: - labels = {'labels': []} - result = gapi.call(gmail.users().settings().filters(), 'get', - soft_errors=True, - userId='me', id=filterId) - if result: - print(f'User: {user}, Filter:{currentCount(i, count)}') - _showFilter(result, 1, 1, labels) - -def doForward(users): - enable = getBoolean(sys.argv[4], 'gam forward') - body = {'enabled': enable} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP: - body['disposition'] = EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP[myarg] - i += 1 - elif myarg == 'confirm': - i += 1 - elif myarg.find('@') != -1: - body['emailAddress'] = sys.argv[i] - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam forward") - if enable and (not body.get('disposition') or not body.get('emailAddress')): - controlflow.system_error_exit(2, 'you must specify an action and a forwarding address for "gam forward') - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - if enable: - print(f'User: {user}, Forward Enabled: {enable}, Forwarding Address: {body["emailAddress"]}, Action: {body["disposition"]}{currentCount(i, count)}') - else: - print(f'User: {user}, Forward Enabled: {enable}{currentCount(i, count)}') - gapi.call(gmail.users().settings(), 'updateAutoForwarding', - soft_errors=True, - userId='me', body=body) - -def printShowForward(users, csvFormat): - def _showForward(user, i, count, result): - if 'enabled' in result: - enabled = result['enabled'] - if enabled: - print(f'User: {user}, Forward Enabled: {enabled}, Forwarding Address: {result["emailAddress"]}, Action: {result["disposition"]}{currentCount(i, count)}') - else: - print(f'User: {user}, Forward Enabled: {enabled}{currentCount(i, count)}') - else: - enabled = result['enable'] == 'true' - if enabled: - print(f'User: {user}, Forward Enabled: {enabled}, Forwarding Address: {result["forwardTo"]}, Action: {EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result["action"]]}{currentCount(i, count)}') - else: - print(f'User: {user}, Forward Enabled: {enabled}{currentCount(i, count)}') - - def _printForward(user, result): - if 'enabled' in result: - row = {'User': user, 'forwardEnabled': result['enabled']} - if result['enabled']: - row['forwardTo'] = result['emailAddress'] - row['disposition'] = result['disposition'] - else: - row = {'User': user, 'forwardEnabled': result['enable']} - if result['enable'] == 'true': - row['forwardTo'] = result['forwardTo'] - row['disposition'] = EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result['action']] - csvRows.append(row) - - if csvFormat: - todrive = False - csvRows = [] - titles = ['User', 'forwardEnabled', 'forwardTo', 'disposition'] - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} forward") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings(), 'getAutoForwarding', - soft_errors=True, - userId='me') - if result: - if not csvFormat: - _showForward(user, i, count, result) - else: - _printForward(user, result) - if csvFormat: - display.write_csv_file(csvRows, titles, 'Forward', todrive) - -def addForwardingAddresses(users): - emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) - body = {'forwardingEmail': emailAddress} - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Adding Forwarding Address {emailAddress} for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings().forwardingAddresses(), 'create', - soft_errors=True, - userId='me', body=body) - -def deleteForwardingAddresses(users): - emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'deleting Forwarding Address {emailAddress} for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings().forwardingAddresses(), 'delete', - soft_errors=True, - userId='me', forwardingEmail=emailAddress) - -def printShowForwardingAddresses(users, csvFormat): - if csvFormat: - todrive = False - csvRows = [] - titles = ['User', 'forwardingEmail', 'verificationStatus'] - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} forwardingaddresses") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings().forwardingAddresses(), 'list', - soft_errors=True, - userId='me') - jcount = len(result.get('forwardingAddresses', [])) if (result) else 0 - if not csvFormat: - print(f'User: {user}, Forwarding Addresses:{currentCount(i, count)}') - if jcount == 0: - continue - j = 0 - for forward in result['forwardingAddresses']: - j += 1 - print(f' Forwarding Address: {forward["forwardingEmail"]}, Verification Status: {forward["verificationStatus"]}{currentCount(j, jcount)}') - else: - if jcount == 0: - continue - for forward in result['forwardingAddresses']: - row = {'User': user, 'forwardingEmail': forward['forwardingEmail'], 'verificationStatus': forward['verificationStatus']} - csvRows.append(row) - if csvFormat: - display.write_csv_file(csvRows, titles, 'Forwarding Addresses', todrive) - -def infoForwardingAddresses(users): - emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - forward = gapi.call(gmail.users().settings().forwardingAddresses(), 'get', - soft_errors=True, - userId='me', forwardingEmail=emailAddress) - if forward: - print(f'User: {user}, Forwarding Address: {forward["forwardingEmail"]}, Verification Status: {forward["verificationStatus"]}{currentCount(i, count)}') - -def doSignature(users): - tagReplacements = {} - i = 4 - if sys.argv[i].lower() == 'file': - filename = sys.argv[i+1] - i, encoding = getCharSet(i+2) - signature = fileutils.read_file(filename, encoding=encoding) - else: - signature = utils.get_string(i, 'String', minLen=0) - i += 1 - body = {} - html = False - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'html': - html = True - i += 1 - else: - i = getSendAsAttributes(i, myarg, body, tagReplacements, 'signature') - body['signature'] = _processSignature(tagReplacements, signature, html) - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Setting Signature for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings().sendAs(), 'patch', - soft_errors=True, - userId='me', body=body, sendAsEmail=user) - -def getSignature(users): - formatSig = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'format': - formatSig = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show signature") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings().sendAs(), 'get', - soft_errors=True, - userId='me', sendAsEmail=user) - if result: - _showSendAs(result, i, count, formatSig) - -def doVacation(users): - enable = getBoolean(sys.argv[4], 'gam vacation') - body = {'enableAutoReply': enable} - if enable: - responseBodyType = 'responseBodyPlainText' - message = None - tagReplacements = {} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'subject': - body['responseSubject'] = sys.argv[i+1] - i += 2 - elif myarg == 'message': - message = sys.argv[i+1] - i += 2 - elif myarg == 'file': - filename = sys.argv[i+1] - i, encoding = getCharSet(i+2) - message = fileutils.read_file(filename, encoding=encoding) - elif myarg == 'replace': - matchTag = utils.get_string(i+1, 'Tag') - matchReplacement = utils.get_string(i+2, 'String', minLen=0) - tagReplacements[matchTag] = matchReplacement - i += 3 - elif myarg == 'html': - responseBodyType = 'responseBodyHtml' - i += 1 - elif myarg == 'contactsonly': - body['restrictToContacts'] = True - i += 1 - elif myarg == 'domainonly': - body['restrictToDomain'] = True - i += 1 - elif myarg == 'startdate': - body['startTime'] = utils.get_yyyymmdd(sys.argv[i+1], returnTimeStamp=True) - i += 2 - elif myarg == 'enddate': - body['endTime'] = utils.get_yyyymmdd(sys.argv[i+1], returnTimeStamp=True) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam vacation") - if message: - if responseBodyType == 'responseBodyHtml': - message = message.replace('\r', '').replace('\\n', '
') - else: - message = message.replace('\r', '').replace('\\n', '\n') - if tagReplacements: - message = _processTags(tagReplacements, message) - body[responseBodyType] = message - if not message and not body.get('responseSubject'): - controlflow.system_error_exit(2, 'You must specify a non-blank subject or message!') - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - print(f'Setting Vacation for {user}{currentCount(i, count)}') - gapi.call(gmail.users().settings(), 'updateVacation', - soft_errors=True, - userId='me', body=body) - -def getVacation(users): - formatReply = False - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'format': - formatReply = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam show vacation") - i = 0 - count = len(users) - for user in users: - i += 1 - user, gmail = buildGmailGAPIObject(user) - if not gmail: - continue - result = gapi.call(gmail.users().settings(), 'getVacation', - soft_errors=True, - userId='me') - if result: - enabled = result['enableAutoReply'] - print(f'User: {user}, Vacation:{currentCount(i, count)}') - print(f' Enabled: {enabled}') - if enabled: - print(f' Contacts Only: {result["restrictToContacts"]}') - print(f' Domain Only: {result["restrictToDomain"]}') - if 'startTime' in result: - print(f' Start Date: {utils.formatTimestampYMD(result["startTime"])}') - else: - print(' Start Date: Started') - if 'endTime' in result: - print(f' End Date: {utils.formatTimestampYMD(result["endTime"])}') - else: - print(' End Date: Not specified') - print(f' Subject: {result.get("responseSubject", "None")}') - sys.stdout.write(' Message:\n ') - if result.get('responseBodyPlainText'): - print(utils.indentMultiLineText(result['responseBodyPlainText'], n=4)) - elif result.get('responseBodyHtml'): - if formatReply: - print(utils.indentMultiLineText(utils.dehtml(result['responseBodyHtml']), n=4)) - else: - print(utils.indentMultiLineText(result['responseBodyHtml'], n=4)) - else: - print('None') - -def doDelSchema(): - cd = buildGAPIObject('directory') - schemaKey = sys.argv[3] - gapi.call(cd.schemas(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaKey) - print(f'Deleted schema {schemaKey}') - -def doCreateOrUpdateUserSchema(updateCmd): - cd = buildGAPIObject('directory') - schemaKey = sys.argv[3] - if updateCmd: - cmd = 'update' - try: - body = gapi.call(cd.schemas(), 'get', throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND], customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaKey) - except gapi.errors.GapiNotFoundError: - controlflow.system_error_exit(3, f'Schema {schemaKey} does not exist.') - else: # create - cmd = 'create' - body = {'schemaName': schemaKey, 'fields': []} - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'field': - if updateCmd: # clear field if it exists on update - for n, field in enumerate(body['fields']): - if field['fieldName'].lower() == sys.argv[i+1].lower(): - del body['fields'][n] - break - a_field = {'fieldName': sys.argv[i+1]} - i += 2 - while True: - myarg = sys.argv[i].lower() - if myarg == 'type': - a_field['fieldType'] = sys.argv[i+1].upper() - validTypes = ['BOOL', 'DOUBLE', 'EMAIL', 'INT64', 'PHONE', 'STRING'] - if a_field['fieldType'] not in validTypes: - controlflow.expected_argument_exit("type", ", ".join(validTypes).lower(), a_field['fieldType']) - i += 2 - elif myarg == 'multivalued': - a_field['multiValued'] = True - i += 1 - elif myarg == 'indexed': - a_field['indexed'] = True - i += 1 - elif myarg == 'restricted': - a_field['readAccessType'] = 'ADMINS_AND_SELF' - i += 1 - elif myarg == 'range': - a_field['numericIndexingSpec'] = {'minValue': getInteger(sys.argv[i+1], myarg), - 'maxValue': getInteger(sys.argv[i+2], myarg)} - i += 3 - elif myarg == 'endfield': - body['fields'].append(a_field) - i += 1 - break - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {cmd} schema") - elif updateCmd and myarg == 'deletefield': - for n, field in enumerate(body['fields']): - if field['fieldName'].lower() == sys.argv[i+1].lower(): - del body['fields'][n] - break - else: - controlflow.system_error_exit(2, f'field {sys.argv[i+1]} not found in schema {schemaKey}') - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {cmd} schema") - if updateCmd: - result = gapi.call(cd.schemas(), 'update', customerId=GC_Values[GC_CUSTOMER_ID], body=body, schemaKey=schemaKey) - print(f'Updated user schema {result["schemaName"]}') - else: - result = gapi.call(cd.schemas(), 'insert', customerId=GC_Values[GC_CUSTOMER_ID], body=body) - print(f'Created user schema {result["schemaName"]}') - -def _showSchema(schema): - print(f'Schema: {schema["schemaName"]}') - for a_key in schema: - if a_key not in ['schemaName', 'fields', 'etag', 'kind']: - print(f' {a_key}: {schema[a_key]}') - for field in schema['fields']: - print(f' Field: {field["fieldName"]}') - for a_key in field: - if a_key not in ['fieldName', 'kind', 'etag']: - print(f' {a_key}: {field[a_key]}') - -def doPrintShowUserSchemas(csvFormat): - cd = buildGAPIObject('directory') - if csvFormat: - todrive = False - csvRows = [] - titles = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} schemas") - schemas = gapi.call(cd.schemas(), 'list', customerId=GC_Values[GC_CUSTOMER_ID]) - if not schemas or 'schemas' not in schemas: - return - for schema in schemas['schemas']: - if not csvFormat: - _showSchema(schema) - else: - row = {'fields.Count': len(schema['fields'])} - display.add_row_titles_to_csv_file(utils.flatten_json(schema, flattened=row), csvRows, titles) - if csvFormat: - display.sort_csv_titles(['schemaId', 'schemaName', 'fields.Count'], titles) - display.write_csv_file(csvRows, titles, 'User Schemas', todrive) - -def doGetUserSchema(): - cd = buildGAPIObject('directory') - schemaKey = sys.argv[3] - schema = gapi.call(cd.schemas(), 'get', customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaKey) - _showSchema(schema) - -def getUserAttributes(i, cd, updateCmd): - def getEntryType(i, entry, entryTypes, setTypeCustom=True, customKeyword='custom', customTypeKeyword='customType'): - """ Get attribute entry type - entryTypes is list of pre-defined types, a|b|c - Allow a|b|c|, a|b|c|custom - setTypeCustom=True, For all fields except organizations, when setting a custom type you do: - entry[u'type'] = u'custom' - entry[u'customType'] = - setTypeCustom=False, For organizations, you don't set entry[u'type'] = u'custom' - Preserve case of custom types - """ - utype = sys.argv[i] - ltype = utype.lower() - if ltype == customKeyword: - i += 1 - utype = sys.argv[i] - ltype = utype.lower() - if ltype in entryTypes: - entry['type'] = ltype - entry.pop(customTypeKeyword, None) - else: - entry[customTypeKeyword] = utype - if setTypeCustom: - entry['type'] = customKeyword - else: - entry.pop('type', None) - return i+1 - - def checkClearBodyList(i, body, itemName): - if sys.argv[i].lower() == 'clear': - body.pop(itemName, None) - body[itemName] = None - return True - return False - - def appendItemToBodyList(body, itemName, itemValue, checkSystemId=False): - if (itemName in body) and (body[itemName] is None): - del body[itemName] - body.setdefault(itemName, []) -# Throw an error if multiple items are marked primary - if itemValue.get('primary', False): - for citem in body[itemName]: - if citem.get('primary', False): - if not checkSystemId or itemValue.get('systemId') == citem.get('systemId'): - controlflow.system_error_exit(2, f'Multiple {itemName} are marked primary, only one can be primary') - body[itemName].append(itemValue) - - def _splitSchemaNameDotFieldName(sn_fn, fnRequired=True): - if sn_fn.find('.') != -1: - schemaName, fieldName = sn_fn.split('.', 1) - schemaName = schemaName.strip() - fieldName = fieldName.strip() - if schemaName and fieldName: - return (schemaName, fieldName) - elif not fnRequired: - schemaName = sn_fn.strip() - if schemaName: - return (schemaName, None) - controlflow.system_error_exit(2, f'{sn_fn} is not a valid custom schema.field name.') - - if updateCmd: - body = {} - need_password = False - else: - body = {'name': {'givenName': 'Unknown', 'familyName': 'Unknown'}} - body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i], noUid=True) - i += 1 - need_password = True - need_to_hash_password = True - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['firstname', 'givenname']: - body.setdefault('name', {}) - body['name']['givenName'] = sys.argv[i+1] - i += 2 - elif myarg in ['lastname', 'familyname']: - body.setdefault('name', {}) - body['name']['familyName'] = sys.argv[i+1] - i += 2 - elif myarg in ['username', 'email', 'primaryemail'] and updateCmd: - body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i+1], noUid=True) - i += 2 - elif myarg == 'customerid' and updateCmd: - body['customerId'] = sys.argv[i+1] - i += 2 - elif myarg == 'password': - need_password = False - body['password'] = sys.argv[i+1] - if body['password'].lower() == 'random': - need_password = True - i += 2 - elif myarg == 'admin': - value = getBoolean(sys.argv[i+1], myarg) - if updateCmd or value: - controlflow.invalid_argument_exit(f"{sys.argv[i]} {value}", f"gam {['create', 'update'][updateCmd]} user") - i += 2 - elif myarg == 'suspended': - body['suspended'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg == 'archived': - body['archived'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg == 'gal': - body['includeInGlobalAddressList'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg in ['sha', 'sha1', 'sha-1']: - body['hashFunction'] = 'SHA-1' - need_to_hash_password = False - i += 1 - elif myarg == 'md5': - body['hashFunction'] = 'MD5' - need_to_hash_password = False - i += 1 - elif myarg == 'crypt': - body['hashFunction'] = 'crypt' - need_to_hash_password = False - i += 1 - elif myarg == 'nohash': - need_to_hash_password = False - i += 1 - elif myarg == 'changepassword': - body['changePasswordAtNextLogin'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg == 'ipwhitelisted': - body['ipWhitelisted'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg == 'agreedtoterms': - body['agreedToTerms'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg in ['org', 'ou']: - body['orgUnitPath'] = getOrgUnitItem(sys.argv[i+1], pathOnly=True) - i += 2 - elif myarg in ['language', 'languages']: - i += 1 - if checkClearBodyList(i, body, 'languages'): - i += 1 - continue - for language in sys.argv[i].replace(',', ' ').split(): - if language.lower() in LANGUAGE_CODES_MAP: - appendItemToBodyList(body, 'languages', {'languageCode': LANGUAGE_CODES_MAP[language.lower()]}) - else: - appendItemToBodyList(body, 'languages', {'customLanguage': language}) - i += 1 - elif myarg == 'gender': - i += 1 - if checkClearBodyList(i, body, 'gender'): - i += 1 - continue - gender = {} - i = getEntryType(i, gender, USER_GENDER_TYPES, customKeyword='other', customTypeKeyword='customGender') - if (i < len(sys.argv)) and (sys.argv[i].lower() == 'addressmeas'): - gender['addressMeAs'] = utils.get_string(i+1, 'String') - i += 2 - body['gender'] = gender - elif myarg in ['address', 'addresses']: - i += 1 - if checkClearBodyList(i, body, 'addresses'): - i += 1 - continue - address = {} - if sys.argv[i].lower() != 'type': - controlflow.system_error_exit(2, f'wrong format for account address details. Expected type got {sys.argv[i]}') - i = getEntryType(i+1, address, USER_ADDRESS_TYPES) - if sys.argv[i].lower() in ['unstructured', 'formatted']: - i += 1 - address['sourceIsStructured'] = False - address['formatted'] = sys.argv[i].replace('\\n', '\n') - i += 1 - while True: - myopt = sys.argv[i].lower() - if myopt == 'pobox': - address['poBox'] = sys.argv[i+1] - i += 2 - elif myopt == 'extendedaddress': - address['extendedAddress'] = sys.argv[i+1] - i += 2 - elif myopt == 'streetaddress': - address['streetAddress'] = sys.argv[i+1] - i += 2 - elif myopt == 'locality': - address['locality'] = sys.argv[i+1] - i += 2 - elif myopt == 'region': - address['region'] = sys.argv[i+1] - i += 2 - elif myopt == 'postalcode': - address['postalCode'] = sys.argv[i+1] - i += 2 - elif myopt == 'country': - address['country'] = sys.argv[i+1] - i += 2 - elif myopt == 'countrycode': - address['countryCode'] = sys.argv[i+1] - i += 2 - elif myopt in ['notprimary', 'primary']: - address['primary'] = myopt == 'primary' - i += 1 - break - else: - controlflow.system_error_exit(2, f'invalid argument ({sys.argv[i]}) for account address details') - appendItemToBodyList(body, 'addresses', address) - elif myarg in ['emails', 'otheremail', 'otheremails']: - i += 1 - if checkClearBodyList(i, body, 'emails'): - i += 1 - continue - an_email = {} - i = getEntryType(i, an_email, USER_EMAIL_TYPES) - an_email['address'] = sys.argv[i] - i += 1 - appendItemToBodyList(body, 'emails', an_email) - elif myarg in ['im', 'ims']: - i += 1 - if checkClearBodyList(i, body, 'ims'): - i += 1 - continue - im = {} - if sys.argv[i].lower() != 'type': - controlflow.system_error_exit(2, f'wrong format for account im details. Expected type got {sys.argv[i]}') - i = getEntryType(i+1, im, USER_IM_TYPES) - if sys.argv[i].lower() != 'protocol': - controlflow.system_error_exit(2, f'wrong format for account details. Expected protocol got {sys.argv[i]}') - i += 1 - im['protocol'] = sys.argv[i].lower() - validProtocols = ['custom_protocol', 'aim', 'gtalk', 'icq', 'jabber', 'msn', 'net_meeting', 'qq', 'skype', 'yahoo'] - if im['protocol'] not in validProtocols: - controlflow.expected_argument_exit("protocol", ", ".join(validProtocols), im['protocol']) - if im['protocol'] == 'custom_protocol': - i += 1 - im['customProtocol'] = sys.argv[i] - i += 1 - # Backwards compatibility: notprimary|primary on either side of IM address - myopt = sys.argv[i].lower() - if myopt in ['notprimary', 'primary']: - im['primary'] = myopt == 'primary' - i += 1 - im['im'] = sys.argv[i] - i += 1 - myopt = sys.argv[i].lower() if i < len(sys.argv) else '' - if myopt in ['notprimary', 'primary']: - im['primary'] = myopt == 'primary' - i += 1 - appendItemToBodyList(body, 'ims', im) - elif myarg in ['organization', 'organizations']: - i += 1 - if checkClearBodyList(i, body, 'organizations'): - i += 1 - continue - organization = {} - while True: - myopt = sys.argv[i].lower() - if myopt == 'name': - organization['name'] = sys.argv[i+1] - i += 2 - elif myopt == 'title': - organization['title'] = sys.argv[i+1] - i += 2 - elif myopt == 'customtype': - organization['customType'] = sys.argv[i+1] - organization.pop('type', None) - i += 2 - elif myopt == 'type': - i = getEntryType(i+1, organization, USER_ORGANIZATION_TYPES, setTypeCustom=False) - elif myopt == 'department': - organization['department'] = sys.argv[i+1] - i += 2 - elif myopt == 'symbol': - organization['symbol'] = sys.argv[i+1] - i += 2 - elif myopt == 'costcenter': - organization['costCenter'] = sys.argv[i+1] - i += 2 - elif myopt == 'location': - organization['location'] = sys.argv[i+1] - i += 2 - elif myopt == 'description': - organization['description'] = sys.argv[i+1] - i += 2 - elif myopt == 'domain': - organization['domain'] = sys.argv[i+1] - i += 2 - elif myopt in ['notprimary', 'primary']: - organization['primary'] = myopt == 'primary' - i += 1 - break - else: - controlflow.system_error_exit(2, f'invalid argument ({sys.argv[i]}) for account organization details') - appendItemToBodyList(body, 'organizations', organization) - elif myarg in ['phone', 'phones']: - i += 1 - if checkClearBodyList(i, body, 'phones'): - i += 1 - continue - phone = {} - while True: - myopt = sys.argv[i].lower() - if myopt == 'value': - phone['value'] = sys.argv[i+1] - i += 2 - elif myopt == 'type': - i = getEntryType(i+1, phone, USER_PHONE_TYPES) - elif myopt in ['notprimary', 'primary']: - phone['primary'] = myopt == 'primary' - i += 1 - break - else: - controlflow.system_error_exit(2, f'invalid argument ({sys.argv[i]}) for account phone details') - appendItemToBodyList(body, 'phones', phone) - elif myarg in ['relation', 'relations']: - i += 1 - if checkClearBodyList(i, body, 'relations'): - i += 1 - continue - relation = {} - i = getEntryType(i, relation, USER_RELATION_TYPES) - relation['value'] = sys.argv[i] - i += 1 - appendItemToBodyList(body, 'relations', relation) - elif myarg in ['externalid', 'externalids']: - i += 1 - if checkClearBodyList(i, body, 'externalIds'): - i += 1 - continue - externalid = {} - i = getEntryType(i, externalid, USER_EXTERNALID_TYPES) - externalid['value'] = sys.argv[i] - i += 1 - appendItemToBodyList(body, 'externalIds', externalid) - elif myarg in ['website', 'websites']: - i += 1 - if checkClearBodyList(i, body, 'websites'): - i += 1 - continue - website = {} - i = getEntryType(i, website, USER_WEBSITE_TYPES) - website['value'] = sys.argv[i] - i += 1 - myopt = sys.argv[i].lower() if i < len(sys.argv) else '' - if myopt in ['notprimary', 'primary']: - website['primary'] = myopt == 'primary' - i += 1 - appendItemToBodyList(body, 'websites', website) - elif myarg in ['note', 'notes']: - i += 1 - if checkClearBodyList(i, body, 'notes'): - i += 1 - continue - note = {} - if sys.argv[i].lower() in ['text_plain', 'text_html']: - note['contentType'] = sys.argv[i].lower() - i += 1 - if sys.argv[i].lower() == 'file': - filename = sys.argv[i+1] - i, encoding = getCharSet(i+2) - note['value'] = fileutils.read_file(filename, encoding=encoding) - else: - note['value'] = sys.argv[i].replace('\\n', '\n') - i += 1 - body['notes'] = note - elif myarg in ['location', 'locations']: - i += 1 - if checkClearBodyList(i, body, 'locations'): - i += 1 - continue - location = {'type': 'desk', 'area': ''} - while True: - myopt = sys.argv[i].lower() - if myopt == 'type': - i = getEntryType(i+1, location, USER_LOCATION_TYPES) - elif myopt == 'area': - location['area'] = sys.argv[i+1] - i += 2 - elif myopt in ['building', 'buildingid']: - location['buildingId'] = gapi.directory.resource.getBuildingByNameOrId(cd, sys.argv[i+1]) - i += 2 - elif myopt in ['desk', 'deskcode']: - location['deskCode'] = sys.argv[i+1] - i += 2 - elif myopt in ['floor', 'floorname']: - location['floorName'] = sys.argv[i+1] - i += 2 - elif myopt in ['section', 'floorsection']: - location['floorSection'] = sys.argv[i+1] - i += 2 - elif myopt in ['endlocation']: - i += 1 - break - else: - controlflow.system_error_exit(3, f'{myopt} is not a valid argument for user location details. Make sure user location details end with an endlocation argument') - appendItemToBodyList(body, 'locations', location) - elif myarg in ['ssh', 'sshkeys', 'sshpublickeys']: - i += 1 - if checkClearBodyList(i, body, 'sshPublicKeys'): - i += 1 - continue - ssh = {} - while True: - myopt = sys.argv[i].lower() - if myopt == 'expires': - ssh['expirationTimeUsec'] = getInteger(sys.argv[i+1], myopt, minVal=0) - i += 2 - elif myopt == 'key': - ssh['key'] = sys.argv[i+1] - i += 2 - elif myopt in ['endssh']: - i += 1 - break - else: - controlflow.system_error_exit(3, f'{myopt} is not a valid argument for user ssh details. Make sure user ssh details end with an endssh argument') - appendItemToBodyList(body, 'sshPublicKeys', ssh) - elif myarg in ['posix', 'posixaccounts']: - i += 1 - if checkClearBodyList(i, body, 'posixAccounts'): - i += 1 - continue - posix = {} - while True: - myopt = sys.argv[i].lower() - if myopt == 'gecos': - posix['gecos'] = sys.argv[i+1] - i += 2 - elif myopt == 'gid': - posix['gid'] = getInteger(sys.argv[i+1], myopt, minVal=0) - i += 2 - elif myopt == 'uid': - posix['uid'] = getInteger(sys.argv[i+1], myopt, minVal=1000) - i += 2 - elif myopt in ['home', 'homedirectory']: - posix['homeDirectory'] = sys.argv[i+1] - i += 2 - elif myopt in ['primary']: - posix['primary'] = getBoolean(sys.argv[i+1], myopt) - i += 2 - elif myopt in ['shell']: - posix['shell'] = sys.argv[i+1] - i += 2 - elif myopt in ['system', 'systemid']: - posix['systemId'] = sys.argv[i+1] - i += 2 - elif myopt in ['username', 'name']: - posix['username'] = sys.argv[i+1] - i += 2 - elif myopt in ['os', 'operatingsystemtype']: - posix['operatingSystemType'] = sys.argv[i+1] - i += 2 - elif myopt in ['endposix']: - i += 1 - break - else: - controlflow.system_error_exit(3, f'{myopt} is not a valid argument for user posix details. Make sure user posix details end with an endposix argument') - appendItemToBodyList(body, 'posixAccounts', posix, checkSystemId=True) - elif myarg in ['keyword', 'keywords']: - i += 1 - if checkClearBodyList(i, body, 'keywords'): - i += 1 - continue - keyword = {} - i = getEntryType(i, keyword, USER_KEYWORD_TYPES) - keyword['value'] = sys.argv[i] - i += 1 - appendItemToBodyList(body, 'keywords', keyword) - elif myarg in ['recoveryemail']: - body['recoveryEmail'] = sys.argv[i+1] - i += 2 - elif myarg in ['recoveryphone']: - body['recoveryPhone'] = sys.argv[i+1] - if body['recoveryPhone'] and body['recoveryPhone'][0] != '+': - body['recoveryPhone'] = '+' + body['recoveryPhone'] - i += 2 - elif myarg == 'clearschema': - if not updateCmd: - controlflow.invalid_argument_exit(sys.argv[i], "gam create user") - schemaName, fieldName = _splitSchemaNameDotFieldName(sys.argv[i+1], False) - up = 'customSchemas' - body.setdefault(up, {}) - body[up].setdefault(schemaName, {}) - if fieldName is None: - schema = gapi.call(cd.schemas(), 'get', - soft_errors=True, - customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaName, fields='fields(fieldName)') - if not schema: - sys.exit(2) - for field in schema['fields']: - body[up][schemaName][field['fieldName']] = None - else: - body[up][schemaName][fieldName] = None - i += 2 - elif myarg.find('.') >= 0: - schemaName, fieldName = _splitSchemaNameDotFieldName(sys.argv[i]) - up = 'customSchemas' - body.setdefault(up, {}) - body[up].setdefault(schemaName, {}) - i += 1 - multivalue = sys.argv[i].lower() - if multivalue in ['multivalue', 'multivalued', 'value', 'multinonempty']: - i += 1 - body[up][schemaName].setdefault(fieldName, []) - schemaValue = {} - if sys.argv[i].lower() == 'type': - i += 1 - schemaValue['type'] = sys.argv[i].lower() - validSchemaTypes = ['custom', 'home', 'other', 'work'] - if schemaValue['type'] not in validSchemaTypes: - controlflow.expected_argument_exit("schema type", ", ".join(validSchemaTypes), schemaValue['type']) - i += 1 - if schemaValue['type'] == 'custom': - schemaValue['customType'] = sys.argv[i] - i += 1 - schemaValue['value'] = sys.argv[i] - if schemaValue['value'] or multivalue != 'multinonempty': - body[up][schemaName][fieldName].append(schemaValue) - else: - body[up][schemaName][fieldName] = sys.argv[i] - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], f"gam {['create', 'update'][updateCmd]} user") - if need_password: - rnd = SystemRandom() - body['password'] = ''.join(rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(100)) - if 'password' in body and need_to_hash_password: - body['password'] = gen_sha512_hash(body['password']) - body['hashFunction'] = 'crypt' - return body - -def getCRMService(login_hint): - scopes = ['https://www.googleapis.com/auth/cloud-platform'] - client_id = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com' - client_secret = 'qM3dP8f_4qedwzWQE1VR4zzU' - creds = auth.oauth.Credentials.from_client_secrets(client_id, client_secret, - scopes, 'online', - login_hint=login_hint, - use_console_flow=not GC_Values[GC_OAUTH_BROWSER]) - httpc = transport.AuthorizedHttp(creds) - return getService('cloudresourcemanagerv1', httpc), httpc - -# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here. -def getCRM2Service(httpc): - return getService('cloudresourcemanager', httpc) - -def getGAMProjectFile(filepath): - # if file exists locally in GAM path then use it. - # allows for testing changes before updating project. - local_file = os.path.join(GM_Globals[GM_GAM_PATH], filepath) - if os.path.isfile(local_file): - return fileutils.read_file(local_file, continue_on_error=False, display_errors=True) - file_url = GAM_PROJECT_FILEPATH+filepath - httpObj = transport.create_http() - _, c = httpObj.request(file_url, 'GET') - return c.decode(UTF8) - -def enableGAMProjectAPIs(GAMProjectAPIs, httpObj, projectId, checkEnabled, i=0, count=0): - apis = GAMProjectAPIs[:] - project_name = f'project:{projectId}' - serveman = getService('servicemanagement', httpObj) - status = True - if checkEnabled: - try: - services = gapi.get_all_pages(serveman.services(), 'list', 'services', - throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND], - consumerId=project_name, fields='nextPageToken,services(serviceName)') - jcount = len(services) - print(f' Project: {projectId}, Check {jcount} APIs{currentCount(i, count)}') - j = 0 - for service in sorted(services, key=lambda k: k['serviceName']): - j += 1 - if 'serviceName' in service: - if service['serviceName'] in apis: - print(f' API: {service["serviceName"]}, Already enabled{currentCount(j, jcount)}') - apis.remove(service['serviceName']) - else: - print(f' API: {service["serviceName"]}, Already enabled (non-GAM which is fine){currentCount(j, jcount)}') - except gapi.errors.GapiNotFoundError as e: - print(f' Project: {projectId}, Update Failed: {str(e)}{currentCount(i, count)}') - status = False - jcount = len(apis) - if status and jcount > 0: - print(f' Project: {projectId}, Enable {jcount} APIs{currentCount(i, count)}') - j = 0 - for api in apis: - j += 1 - while True: - try: - gapi.call(serveman.services(), 'enable', - throw_reasons=[gapi.errors.ErrorReason.FAILED_PRECONDITION, gapi.errors.ErrorReason.FORBIDDEN, gapi.errors.ErrorReason.PERMISSION_DENIED], - serviceName=api, body={'consumerId': project_name}) - print(f' API: {api}, Enabled{currentCount(j, jcount)}') - break - except gapi.errors.GapiFailedPreconditionError as e: - print(f'\nThere was an error enabling {api}. Please resolve error as described below:') - print() - print(f'\n{str(e)}\n') - print() - input('Press enter once resolved and we will try enabling the API again.') - except (gapi.errors.GapiForbiddenError, gapi.errors.GapiPermissionDeniedError) as e: - print(f' API: {api}, Enable Failed: {str(e)}{currentCount(j, jcount)}') - status = False - return status - -def _grantSARotateRights(iam, sa_email): - print(f'Giving service account {sa_email} rights to rotate own private key') - body = { - 'policy': { - 'bindings': [ - { - 'role': 'roles/iam.serviceAccountKeyAdmin', - 'members': [f'serviceAccount:{sa_email}'] - } - ] - } - } - gapi.call(iam.projects().serviceAccounts(), 'setIamPolicy', resource=f'projects/-/serviceAccounts/{sa_email}', - body=body) - -def setGAMProjectConsentScreen(httpObj, projectId, login_hint): - print('Setting GAM project consent screen...') - iap = getService('iap', httpObj) - body = {'applicationTitle': 'GAM', 'supportEmail': login_hint} - gapi.call(iap.projects().brands(), 'create', - parent=f'projects/{projectId}', body=body) - -def _createClientSecretsOauth2service(httpObj, projectId, login_hint, create_project): - - def _checkClientAndSecret(simplehttp, client_id, client_secret): - url = 'https://oauth2.googleapis.com/token' - post_data = {'client_id': client_id, 'client_secret': client_secret, - 'code': 'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid', - 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', 'grant_type': 'authorization_code'} - headers = {'Content-type': 'application/x-www-form-urlencoded'} - _, content = simplehttp.request(url, 'POST', urlencode(post_data), headers=headers) - try: - content = json.loads(content) - except ValueError: - print(f'Unknown error: {content}') - return False - if not 'error' in content or not 'error_description' in content: - print(f'Unknown error: {content}') - return False - if content['error'] == 'invalid_grant': - return True - if content['error_description'] == 'The OAuth client was not found.': - print(f'Ooops!!\n\n{client_id}\n\nIs not a valid client ID. Please make sure you are following the directions exactly and that there are no extra spaces in your client ID.') - return False - if content['error_description'] == 'Unauthorized': - print(f'Ooops!!\n\n{client_secret}\n\nIs not a valid client secret. Please make sure you are following the directions exactly and that there are no extra spaces in your client secret.') - return False - print(f'Unknown error: {content}') - return False - - GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines() - enableGAMProjectAPIs(GAMProjectAPIs, httpObj, projectId, False) - if create_project: - setGAMProjectConsentScreen(httpObj, projectId, login_hint) - iam = getService('iam', httpObj) - sa_list = gapi.call(iam.projects().serviceAccounts(), 'list', - name=f'projects/{projectId}') - service_account = None - if 'accounts' in sa_list: - for account in sa_list['accounts']: - sa_email = f'{projectId}@{projectId}.iam.gserviceaccount.com' - if sa_email in account['name']: - service_account = account - break - if not service_account: - print('Creating Service Account') - service_account = gapi.call(iam.projects().serviceAccounts(), 'create', - name=f'projects/{projectId}', - body={'accountId': projectId, 'serviceAccount': {'displayName': 'GAM Project'}}) - GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = service_account['uniqueId'] - doCreateOrRotateServiceAccountKeys(iam, project_id=service_account['projectId'], - client_email=service_account['email'], - client_id=service_account['uniqueId']) - _grantSARotateRights(iam, service_account['name'].rsplit('/', 1)[-1]) - console_url = f'https://console.cloud.google.com/apis/credentials/oauthclient?project={projectId}' - while True: - print(f'''Please go to: - -{console_url} - -1. Choose "Desktop App" or "Other" for "Application type". -2. Enter a desired value for "Name" or leave as is. -3. Click the blue "Create" button. -4. Copy the "client ID" value that shows on the next page. - -''') -# If you use Firefox to copy the Client ID and Secret, the data has leading and trailing newlines -# The first raw_input will get the leading newline, thus we have to issue another raw_input to get the data -# If the newlines are not present, the data is correctly read with the first raw_input - client_id = input('Enter your Client ID: ').strip() - if not client_id: - client_id = input().strip() - print('\nNow go back to your browser and copy your client secret.') - client_secret = input('Enter your Client Secret: ').strip() - if not client_secret: - client_secret = input().strip() - simplehttp = transport.create_http() - client_valid = _checkClientAndSecret(simplehttp, client_id, client_secret) - if client_valid: - break - print() - cs_data = f'''{{ - "installed": {{ - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth", - "client_id": "{client_id}", - "client_secret": "{client_secret}", - "created_by": "{login_hint}", - "project_id": "{projectId}", - "redirect_uris": ["http://localhost", "urn:ietf:wg:oauth:2.0:oob"], - "token_uri": "https://oauth2.googleapis.com/token" - }} -}}''' - fileutils.write_file(GC_Values[GC_CLIENT_SECRETS_JSON], cs_data, continue_on_error=False) - print('That\'s it! Your GAM Project is created and ready to use.') - -VALIDEMAIL_PATTERN = re.compile(r'^[^@]+@[^@]+\.[^@]+$') - -def _getValidateLoginHint(login_hint=None): - while True: - if not login_hint: - login_hint = input('\nWhat is your G Suite admin email address? ').strip() - if login_hint.find('@') == -1 and GC_Values[GC_DOMAIN]: - login_hint = f'{login_hint}@{GC_Values[GC_DOMAIN].lower()}' - if VALIDEMAIL_PATTERN.match(login_hint): - return login_hint - print(f'{ERROR_PREFIX}Invalid email address: {login_hint}') - login_hint = None - -def _getCurrentProjectID(): - cs_data = fileutils.read_file(GC_Values[GC_CLIENT_SECRETS_JSON], continue_on_error=True, display_errors=True) - if not cs_data: - controlflow.system_error_exit(14, f'Your client secrets file:\n\n{GC_Values[GC_CLIENT_SECRETS_JSON]}\n\nis missing. Please recreate the file.') - try: - return json.loads(cs_data)['installed']['project_id'] - except (ValueError, IndexError, KeyError): - controlflow.system_error_exit(3, f'The format of your client secrets file:\n\n{GC_Values[GC_CLIENT_SECRETS_JSON]}\n\nis incorrect. Please recreate the file.') - -def _getProjects(crm, pfilter): - try: - return gapi.get_all_pages(crm.projects(), 'list', 'projects', throw_reasons=[gapi.errors.ErrorReason.BAD_REQUEST], filter=pfilter) - except gapi.errors.GapiBadRequestError as e: - controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}') - -PROJECTID_PATTERN = re.compile(r'^[a-z][a-z0-9-]{4,28}[a-z0-9]$') -PROJECTID_FORMAT_REQUIRED = '[a-z][a-z0-9-]{4,28}[a-z0-9]' - -def _getLoginHintProjectId(createCmd): - login_hint = None - projectId = None - parent = None - if len(sys.argv) >= 4 and sys.argv[3].lower() not in ['admin', 'project', 'parent']: - # legacy "gam create/use project - try: - login_hint = sys.argv[3] - projectId = sys.argv[4] - except IndexError: - pass - else: - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'admin': - login_hint = sys.argv[i+1] - i += 2 - elif myarg == 'project': - projectId = sys.argv[i+1] - i += 2 - elif createCmd and myarg == 'parent': - parent = sys.argv[i+1] - i += 2 - else: - expected = ['admin', 'project'] - if createCmd: - expected.append('parent') - controlflow.system_error_exit(3, f'{myarg} is not a valid argument for "gam {["use", "create"][createCmd]} project", expected one of: {", ".join(expected)}') - login_hint = _getValidateLoginHint(login_hint) - if projectId: - if not PROJECTID_PATTERN.match(projectId): - controlflow.system_error_exit(2, f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>') - elif createCmd: - projectId = 'gam-project' - for _ in range(3): - projectId += f'-{"".join(random.choice(LOWERNUMERIC_CHARS) for _ in range(3))}' - else: - projectId = input('\nWhat is your API project ID? ').strip() - if not PROJECTID_PATTERN.match(projectId): - controlflow.system_error_exit(2, f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>') - crm, httpObj = getCRMService(login_hint) - if parent and not parent.startswith('organizations/') and not parent.startswith('folders/'): - crm2 = getCRM2Service(httpObj) - parent = convertGCPFolderNameToID(parent, crm2) - if parent: - parent_type, parent_id = parent.split('/') - if parent_type[-1] == 's': - parent_type = parent_type[:-1] # folders > folder, organizations > organization - parent = {'type': parent_type, 'id': parent_id} - projects = _getProjects(crm, f'id:{projectId}') - if not createCmd: - if not projects: - controlflow.system_error_exit(2, f'User: {login_hint}, Project ID: {projectId}, Does not exist') - if projects[0]['lifecycleState'] != 'ACTIVE': - controlflow.system_error_exit(2, f'User: {login_hint}, Project ID: {projectId}, Not active') - else: - if projects: - controlflow.system_error_exit(2, f'User: {login_hint}, Project ID: {projectId}, Duplicate') - return (crm, httpObj, login_hint, projectId, parent) - -PROJECTID_FILTER_REQUIRED = 'gam||(filter )' -def convertGCPFolderNameToID(parent, crm2): - # crm2.folders() is broken requiring pageToken, etc in body, not URL. - # for now just use gapi.get_items and if user has that many folders they'll - # just need to be specific. - folders = gapi.get_items(crm2.folders(), 'search', items='folders', - body={'pageSize': 1000, 'query': f'displayName="{parent}"'}) - if not folders: - controlflow.system_error_exit(1, f'ERROR: No folder found matching displayName={parent}') - if len(folders) > 1: - print('Multiple matches:') - for folder in folders: - print(f' Name: {folder["name"]} ID: {folder["displayName"]}') - controlflow.system_error_exit(2, 'ERROR: Multiple matching folders, please specify one.') - return folders[0]['name'] - -def createGCPFolder(): - login_hint = _getValidateLoginHint() - _, httpObj = getCRMService(login_hint) - crm2 = getCRM2Service(httpObj) - gapi.call(crm2.folders(), 'create', body={'name': sys.argv[3], 'displayName': sys.argv[3]}) - -def _getLoginHintProjects(printShowCmd): - login_hint = None - pfilter = None - i = 3 - try: - login_hint = sys.argv[i] - i += 1 - pfilter = sys.argv[i] - i += 1 - except IndexError: - pass - if not pfilter: - pfilter = 'current' if not printShowCmd else 'id:gam-project-*' - elif printShowCmd and pfilter.lower() == 'all': - pfilter = None - elif pfilter.lower() == 'gam': - pfilter = 'id:gam-project-*' - elif pfilter.lower() == 'filter': - pfilter = sys.argv[i] - i += 1 - elif PROJECTID_PATTERN.match(pfilter): - pfilter = f'id:{pfilter}' - else: - controlflow.system_error_exit(2, f'Invalid Project ID: {pfilter}, expected <{["", "all|"][printShowCmd]}{PROJECTID_FILTER_REQUIRED}>') - login_hint = _getValidateLoginHint(login_hint) - crm, httpObj = getCRMService(login_hint) - if pfilter in ['current', 'id:current']: - projectID = _getCurrentProjectID() - if not printShowCmd: - projects = [{'projectId': projectID}] - else: - projects = _getProjects(crm, f'id:{projectID}') - else: - projects = _getProjects(crm, pfilter) - return (crm, httpObj, login_hint, projects, i) - -def _checkForExistingProjectFiles(): - for a_file in [GC_Values[GC_OAUTH2SERVICE_JSON], GC_Values[GC_CLIENT_SECRETS_JSON]]: - if os.path.exists(a_file): - controlflow.system_error_exit(5, f'{a_file} already exists. Please delete or rename it before attempting to use another project.') - -def doCreateProject(): - _checkForExistingProjectFiles() - crm, httpObj, login_hint, projectId, parent = _getLoginHintProjectId(True) - login_domain = login_hint[login_hint.find('@')+1:] - body = {'projectId': projectId, 'name': 'GAM Project'} - if parent: - body['parent'] = parent - while True: - create_again = False - print(f'Creating project "{body["name"]}"...') - create_operation = gapi.call(crm.projects(), 'create', body=body) - operation_name = create_operation['name'] - time.sleep(8) # Google recommends always waiting at least 5 seconds - for i in range(1, 5): - print('Checking project status...') - status = gapi.call(crm.operations(), 'get', - name=operation_name) - if 'error' in status: - if status['error'].get('message', '') == 'No permission to create project in organization': - print('Hmm... Looks like you have no rights to your Google Cloud Organization.') - print('Attempting to fix that...') - getorg = gapi.call(crm.organizations(), 'search', - body={'filter': f'domain:{login_domain}'}) - try: - organization = getorg['organizations'][0]['name'] - print(f'Your organization name is {organization}') - except (KeyError, IndexError): - controlflow.system_error_exit(3, 'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.') - org_policy = gapi.call(crm.organizations(), 'getIamPolicy', - resource=organization) - if 'bindings' not in org_policy: - org_policy['bindings'] = [] - print('Looks like no one has rights to your Google Cloud Organization. Attempting to give you create rights...') - else: - print('The following rights seem to exist:') - for a_policy in org_policy['bindings']: - if 'role' in a_policy: - print(f' Role: {a_policy["role"]}') - if 'members' in a_policy: - print(' Members:') - for member in a_policy['members']: - print(f' {member}') - print() - my_role = 'roles/resourcemanager.projectCreator' - print(f'Giving {login_hint} the role of {my_role}...') - org_policy['bindings'].append({'role': my_role, 'members': [f'user:{login_hint}']}) - gapi.call(crm.organizations(), 'setIamPolicy', - resource=organization, body={'policy': org_policy}) - create_again = True - break - try: - if status['error']['details'][0]['violations'][0]['description'] == 'Callers must accept Terms of Service': - print('''Please go to: - -https://console.cloud.google.com/start - -and accept the Terms of Service (ToS). As soon as you've accepted the ToS popup, you can return here and press enter.''') - input() - create_again = True - break - except (IndexError, KeyError): - pass - controlflow.system_error_exit(1, status) - if status.get('done', False): - break - sleep_time = i ** 2 - print(f'Project still being created. Sleeping {sleep_time} seconds') - time.sleep(sleep_time) - if create_again: - continue - if not status.get('done', False): - controlflow.system_error_exit(1, f'Failed to create project: {status}') - elif 'error' in status: - controlflow.system_error_exit(2, status['error']) - break - _createClientSecretsOauth2service(httpObj, projectId, login_hint, True) - -def doUseProject(): - _checkForExistingProjectFiles() - _, httpObj, login_hint, projectId, _ = _getLoginHintProjectId(False) - _createClientSecretsOauth2service(httpObj, projectId, login_hint, False) - -def doUpdateProjects(): - _, httpObj, login_hint, projects, _ = _getLoginHintProjects(False) - GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines() - count = len(projects) - print(f'User: {login_hint}, Update {count} Projects') - i = 0 - for project in projects: - i += 1 - projectId = project['projectId'] - enableGAMProjectAPIs(GAMProjectAPIs, httpObj, projectId, True, i, count) - iam = getService('iam', httpObj) - _getSvcAcctData() # needed to read in GM_OAUTH2SERVICE_JSON_DATA - sa_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email'] - _grantSARotateRights(iam, sa_email) - -def _generatePrivateKeyAndPublicCert(client_id, key_size): - print(' Generating new private key...') - private_key = rsa.generate_private_key(public_exponent=65537, - key_size=key_size, backend=default_backend()) - private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption()).decode() - print(' Extracting public certificate...') - public_key = private_key.public_key() - builder = x509.CertificateBuilder() - builder = builder.subject_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, client_id)])) - builder = builder.issuer_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, client_id)])) - not_valid_before = datetime.datetime.today() - datetime.timedelta(days=1) - not_valid_after = datetime.datetime.today() + datetime.timedelta(days=365*10-1) - builder = builder.not_valid_before(not_valid_before) - builder = builder.not_valid_after(not_valid_after) - builder = builder.serial_number(x509.random_serial_number()) - builder = builder.public_key(public_key) - builder = builder.add_extension(x509.BasicConstraints(ca=False, - path_length=None), critical=True) - builder = builder.add_extension(x509.KeyUsage(key_cert_sign=False, - crl_sign=False, digital_signature=True, content_commitment=False, - key_encipherment=False, data_encipherment=False, key_agreement=False, - encipher_only=False, decipher_only=False), critical=True) - builder = builder.add_extension( - x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), - critical=True) - certificate = builder.sign(private_key=private_key, - algorithm=hashes.SHA256(), backend=default_backend()) - public_cert_pem = certificate.public_bytes(serialization.Encoding.PEM).decode() - publicKeyData = base64.b64encode(public_cert_pem.encode()) - if isinstance(publicKeyData, bytes): - publicKeyData = publicKeyData.decode() - print(' Done generating private key and public certificate.') - return private_pem, publicKeyData - -def _formatOAuth2ServiceData(project_id, client_email, client_id, private_key, private_key_id): - quoted_email = quote(client_email) - key_json = { - 'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs', - 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', - 'client_email': client_email, - 'client_id': client_id, - 'client_x509_cert_url': f'https://www.googleapis.com/robot/v1/metadata/x509/{quoted_email}', - 'private_key': private_key, - 'private_key_id': private_key_id, - 'project_id': project_id, - 'token_uri': 'https://oauth2.googleapis.com/token', - 'type': 'service_account', - } - return json.dumps(key_json, indent=2, sort_keys=True) - -def doShowServiceAccountKeys(): - iam = buildGAPIServiceObject('iam', None) - keyTypes = None - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'all': - keyTypes = None - i += 1 - elif myarg in ['system', 'systemmanaged']: - keyTypes = 'SYSTEM_MANAGED' - i += 1 - elif myarg in ['user', 'usermanaged']: - keyTypes = 'USER_MANAGED' - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam show sakeys") - name = f'projects/-/serviceAccounts/{GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]}' - currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] - keys = gapi.get_items(iam.projects().serviceAccounts().keys(), 'list', 'keys', - name=name, keyTypes=keyTypes) - if not keys: - print('No keys') - return - parts = keys[0]['name'].rsplit('/') - for i in range(0, 4, 2): - print(f'{parts[i][:-1]}: {parts[i+1]}') - for key in keys: - key['name'] = key['name'].rsplit('/', 1)[-1] - if key['name'] == currentPrivateKeyId: - key['usedToAuthenticateThisRequest'] = True - display.print_json(keys) - -def doCreateOrRotateServiceAccountKeys(iam=None, project_id=None, client_email=None, client_id=None): - local_key_size = 2048 - body = {} - if iam: - mode = 'retainexisting' - else: - mode = 'retainnone' - i = 3 - iam = buildGAPIServiceObject('iam', None) - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'algorithm': - body['keyAlgorithm'] = sys.argv[i+1].upper() - allowed_algorithms = gapi.get_enum_values_minus_unspecified(iam._rootDesc['schemas']['CreateServiceAccountKeyRequest']['properties']['keyAlgorithm']['enum']) - if body['keyAlgorithm'] not in allowed_algorithms: - controlflow.expected_argument_exit("algorithm", ", ".join(allowed_algorithms), body['keyAlgorithm']) - local_key_size = 0 - i += 2 - elif myarg == 'localkeysize': - local_key_size = int(sys.argv[i+1]) - if local_key_size not in [1024, 2048, 4096]: - controlflow.system_error_exit(3, 'localkeysize must be 1024, 2048 or 4096. 1024 is weak and dangerous. 2048 is recommended. 4096 is slow.') - i += 2 - elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']: - mode = myarg - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam rotate sakeys") - currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] - project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id'] - client_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email'] - client_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_id'] - clientId = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] - name = f'projects/-/serviceAccounts/{clientId}' - if mode != 'retainexisting': - keys = gapi.get_items(iam.projects().serviceAccounts().keys(), 'list', 'keys', - name=name, keyTypes='USER_MANAGED') - if local_key_size: - private_key, publicKeyData = _generatePrivateKeyAndPublicCert(name, local_key_size) - print(' Uploading new public certificate to Google...') - result = gapi.call(iam.projects().serviceAccounts().keys(), 'upload', - name=name, body={'publicKeyData': publicKeyData}) - private_key_id = result['name'].rsplit('/', 1)[-1] - oauth2service_data = _formatOAuth2ServiceData(project_id, client_email, client_id, private_key, private_key_id) - else: - result = gapi.call(iam.projects().serviceAccounts().keys(), 'create', name=name, body=body) - oauth2service_data = base64.b64decode(result['privateKeyData']).decode(UTF8) - private_key_id = result['name'].rsplit('/', 1)[-1] - fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON], oauth2service_data, continue_on_error=False) - print(f' Wrote new private key {private_key_id} to {GC_Values[GC_OAUTH2SERVICE_JSON]}') - if mode != 'retainexisting': - count = len(keys) if mode == 'retainnone' else 1 - print(f' Revoking {count} existing key(s) for Service Account {clientId}') - for key in keys: - keyName = key['name'].rsplit('/', 1)[-1] - if mode == 'retainnone' or keyName == currentPrivateKeyId: - print(f' Revoking existing key {keyName} for service account') - gapi.call(iam.projects().serviceAccounts().keys(), 'delete', name=key['name']) - if mode != 'retainnone': - break - -def doDeleteServiceAccountKeys(): - iam = buildGAPIServiceObject('iam', None) - doit = False - keyList = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'doit': - doit = True - i += 1 - else: - keyList.extend(sys.argv[i].replace(',', ' ').split()) - i += 1 - clientId = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] - currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] - name = f'projects/-/serviceAccounts/{clientId}' - keys = gapi.get_items(iam.projects().serviceAccounts().keys(), 'list', 'keys', - name=name, keyTypes='USER_MANAGED') - print(f' Service Account {clientId} has {len(keys)} existing key(s)') - for dkeyName in keyList: - for key in keys: - keyName = key['name'].rsplit('/', 1)[-1] - if dkeyName == keyName: - if keyName == currentPrivateKeyId and not doit: - print(f' Current existing key {keyName} for service account not revoked because doit argument not specified ') - break - print(f' Revoking existing key {keyName} for service account') - gapi.call(iam.projects().serviceAccounts().keys(), 'delete', name=key['name']) - break - else: - print(f' Existing key {dkeyName} for service account not found') - -def doDelProjects(): - crm, _, login_hint, projects, _ = _getLoginHintProjects(False) - count = len(projects) - print(f'User: {login_hint}, Delete {count} Projects') - i = 0 - for project in projects: - i += 1 - projectId = project['projectId'] - try: - gapi.call(crm.projects(), 'delete', throw_reasons=[gapi.errors.ErrorReason.FORBIDDEN], projectId=projectId) - print(f' Project: {projectId} Deleted{currentCount(i, count)}') - except gapi.errors.GapiForbiddenError as e: - print(f' Project: {projectId} Delete Failed: {str(e)}{currentCount(i, count)}') - -def doPrintShowProjects(csvFormat): - _, _, login_hint, projects, i = _getLoginHintProjects(True) - if csvFormat: - csvRows = [] - todrive = False - titles = ['User', 'projectId', 'projectNumber', 'name', 'createTime', 'lifecycleState'] - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} projects") - if not csvFormat: - count = len(projects) - print(f'User: {login_hint}, Show {count} Projects') - i = 0 - for project in projects: - i += 1 - print(f' Project: {project["projectId"]}{currentCount(i, count)}') - print(f' projectNumber: {project["projectNumber"]}') - print(f' name: {project["name"]}') - print(f' createTime: {project["createTime"]}') - print(f' lifecycleState: {project["lifecycleState"]}') - jcount = len(project.get('labels', [])) - if jcount > 0: - print(' labels:') - for k, v in list(project['labels'].items()): - print(f' {k}: {v}') - if 'parent' in project: - print(' parent:') - print(f' type: {project["parent"]["type"]}') - print(f' id: {project["parent"]["id"]}') - else: - for project in projects: - display.add_row_titles_to_csv_file(utils.flatten_json(project, flattened={'User': login_hint}), csvRows, titles) - display.write_csv_file(csvRows, titles, 'Projects', todrive) - -def doGetTeamDriveInfo(users): - teamDriveId = sys.argv[5] - useDomainAdminAccess = False - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam show teamdrive") - for user in users: - drive = buildGAPIServiceObject('drive3', user) - if not drive: - print(f'Failed to access Drive as {user}') - continue - result = gapi.call(drive.drives(), 'get', driveId=teamDriveId, - useDomainAdminAccess=useDomainAdminAccess, fields='*') - display.print_json(result) - -def doCreateTeamDrive(users): - body = {'name': sys.argv[5]} - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'theme': - body['themeId'] = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam create teamdrive") - for user in users: - drive = buildGAPIServiceObject('drive3', user) - if not drive: - print(f'Failed to access Drive as {user}') - continue - requestId = str(uuid.uuid4()) - result = gapi.call(drive.drives(), 'create', requestId=requestId, body=body, fields='id') - print(f'Created Team Drive {body["name"]} with id {result["id"]}') - -TEAMDRIVE_RESTRICTIONS_MAP = { - 'adminmanagedrestrictions': 'adminManagedRestrictions', - 'copyrequireswriterpermission': 'copyRequiresWriterPermission', - 'domainusersonly': 'domainUsersOnly', - 'teammembersonly': 'teamMembersOnly', - } - -def doUpdateTeamDrive(users): - teamDriveId = sys.argv[5] - body = {} - useDomainAdminAccess = False - i = 6 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'name': - body['name'] = sys.argv[i+1] - i += 2 - elif myarg == 'theme': - body['themeId'] = sys.argv[i+1] - i += 2 - elif myarg == 'customtheme': - body['backgroundImageFile'] = { - 'id': sys.argv[i+1], - 'xCoordinate': float(sys.argv[i+2]), - 'yCoordinate': float(sys.argv[i+3]), - 'width': float(sys.argv[i+4]) - } - i += 5 - elif myarg == 'color': - body['colorRgb'] = getColor(sys.argv[i+1]) - i += 2 - elif myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - elif myarg in TEAMDRIVE_RESTRICTIONS_MAP: - body.setdefault('restrictions', {}) - body['restrictions'][TEAMDRIVE_RESTRICTIONS_MAP[myarg]] = getBoolean(sys.argv[i+1], myarg) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam update teamdrive") - if not body: - controlflow.system_error_exit(4, 'nothing to update. Need at least a name argument.') - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - result = gapi.call(drive.drives(), 'update', - useDomainAdminAccess=useDomainAdminAccess, body=body, driveId=teamDriveId, fields='id', soft_errors=True) - if not result: - continue - print(f'Updated Team Drive {teamDriveId}') - -def printShowTeamDrives(users, csvFormat): - todrive = False - useDomainAdminAccess = False - q = None - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'asadmin': - useDomainAdminAccess = True - i += 1 - elif myarg == 'query': - q = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(myarg, "gam print|show teamdrives") - tds = [] - for user in users: - sys.stderr.write(f'Getting Team Drives for {user}\n') - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - results = gapi.get_all_pages(drive.drives(), 'list', 'drives', - useDomainAdminAccess=useDomainAdminAccess, fields='*', - q=q, soft_errors=True) - if not results: - continue - for td in results: - if 'id' not in td: - continue - if 'name' not in td: - td['name'] = '' - this_td = {'id': td['id'], 'name': td['name']} - if this_td in tds: - continue - tds.append({'id': td['id'], 'name': td['name']}) - if csvFormat: - titles = ['name', 'id'] - display.write_csv_file(tds, titles, 'Team Drives', todrive) - else: - for td in tds: - print(f'Name: {td["name"]} ID: {td["id"]}') - -def doDeleteTeamDrive(users): - teamDriveId = sys.argv[5] - for user in users: - user, drive = buildDrive3GAPIObject(user) - if not drive: - continue - print(f'Deleting Team Drive {teamDriveId}') - gapi.call(drive.drives(), 'delete', driveId=teamDriveId, soft_errors=True) - -def extract_nested_zip(zippedFile, toFolder, spacing=' '): - """ Extract a zip file including any nested zip files - Delete the zip file(s) after extraction - """ - print(f'{spacing}extracting {zippedFile}') - with zipfile.ZipFile(zippedFile, 'r') as zfile: - inner_files = zfile.infolist() - for inner_file in inner_files: - print(f'{spacing} {inner_file.filename}') - inner_file_path = zfile.extract(inner_file, toFolder) - if re.search(r'\.zip$', inner_file.filename): - extract_nested_zip(inner_file_path, toFolder, spacing=spacing+' ') - os.remove(zippedFile) - -def doCreateUser(): - cd = buildGAPIObject('directory') - body = getUserAttributes(3, cd, False) - print(f'Creating account for {body["primaryEmail"]}') - gapi.call(cd.users(), 'insert', body=body, fields='primaryEmail') - -def GroupIsAbuseOrPostmaster(emailAddr): - return emailAddr.startswith('abuse@') or emailAddr.startswith('postmaster@') - -GROUP_SETTINGS_LIST_PATTERN = re.compile(r'([A-Z][A-Z_]+[A-Z]?)') - -def getGroupAttrValue(myarg, value, gs_object, gs_body, function): - if myarg == 'collaborative': - myarg = 'enablecollaborativeinbox' - for (attrib, params) in list(gs_object['schemas']['Groups']['properties'].items()): - if attrib in ['kind', 'etag', 'email']: - continue - if myarg == attrib.lower(): - if params['type'] == 'integer': - try: - if value[-1:].upper() == 'M': - value = int(value[:-1]) * 1024 * 1024 - elif value[-1:].upper() == 'K': - value = int(value[:-1]) * 1024 - elif value[-1].upper() == 'B': - value = int(value[:-1]) - else: - value = int(value) - except ValueError: - controlflow.system_error_exit(2, f'{myarg} must be a number ending with M (megabytes), K (kilobytes) or nothing (bytes); got {value}') - elif params['type'] == 'string': - if attrib == 'description': - value = value.replace('\\n', '\n') - elif attrib == 'primaryLanguage': - value = LANGUAGE_CODES_MAP.get(value.lower(), value) - elif attrib in GROUP_SETTINGS_LIST_ATTRIBUTES: - value = value.upper() - possible_values = GROUP_SETTINGS_LIST_PATTERN.findall(params['description']) - if value not in possible_values: - controlflow.expected_argument_exit(f"value for {attrib}", ", ".join(possible_values), value) - elif attrib in GROUP_SETTINGS_BOOLEAN_ATTRIBUTES: - value = value.lower() - if value in true_values: - value = 'true' - elif value in false_values: - value = 'false' - else: - controlflow.expected_argument_exit(f"value for {attrib}", ", ".join(['true', 'false']), value) - gs_body[attrib] = value - return - controlflow.invalid_argument_exit(myarg, f"gam {function} group") - -def doCreateGroup(): - cd = buildGAPIObject('directory') - body = {'email': normalizeEmailAddressOrUID(sys.argv[3], noUid=True)} - gs_get_before_update = got_name = False - i = 4 - gs_body = {} - gs = None - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'name': - body['name'] = sys.argv[i+1] - got_name = True - i += 2 - elif myarg == 'description': - description = sys.argv[i+1].replace('\\n', '\n') -# The Directory API Groups insert method can not handle any of these characters ('\n<>=') in the description field -# If any of these characters are present, use the Group Settings API to set the description - for c in '\n<>=': - if description.find(c) != -1: - gs_body['description'] = description - if not gs: - gs = buildGAPIObject('groupssettings') - gs_object = gs._rootDesc - break - else: - body['description'] = description - i += 2 - elif myarg == 'getbeforeupdate': - gs_get_before_update = True - i += 1 - else: - if not gs: - gs = buildGAPIObject('groupssettings') - gs_object = gs._rootDesc - getGroupAttrValue(myarg, sys.argv[i+1], gs_object, gs_body, 'create') - i += 2 - if not got_name: - body['name'] = body['email'] - print(f'Creating group {body["email"]}') - gapi.call(cd.groups(), 'insert', body=body, fields='email') - if gs and not GroupIsAbuseOrPostmaster(body['email']): - if gs_get_before_update: - current_settings = gapi.call(gs.groups(), 'get', - retry_reasons=[gapi.errors.ErrorReason.SERVICE_LIMIT, gapi.errors.ErrorReason.NOT_FOUND], - groupUniqueId=body['email'], fields='*') - if current_settings is not None: - gs_body = dict(list(current_settings.items()) + list(gs_body.items())) - if gs_body: - gapi.call(gs.groups(), 'update', groupUniqueId=body['email'], - retry_reasons=[gapi.errors.ErrorReason.SERVICE_LIMIT, - gapi.errors.ErrorReason.NOT_FOUND], - body=gs_body) - -def doCreateAlias(): - cd = buildGAPIObject('directory') - body = {'alias': normalizeEmailAddressOrUID(sys.argv[3], noUid=True, noLower=True)} - target_type = sys.argv[4].lower() - if target_type not in ['user', 'group', 'target']: - controlflow.expected_argument_exit("target type", ", ".join(['user', 'group', 'target']), target_type) - targetKey = normalizeEmailAddressOrUID(sys.argv[5]) - print(f'Creating alias {body["alias"]} for {target_type} {targetKey}') - if target_type == 'user': - gapi.call(cd.users().aliases(), 'insert', userKey=targetKey, body=body) - elif target_type == 'group': - gapi.call(cd.groups().aliases(), 'insert', groupKey=targetKey, body=body) - elif target_type == 'target': - try: - gapi.call(cd.users().aliases(), 'insert', throw_reasons=[gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.BAD_REQUEST], userKey=targetKey, body=body) - except (gapi.errors.GapiInvalidError, gapi.errors.GapiBadRequestError): - gapi.call(cd.groups().aliases(), 'insert', groupKey=targetKey, body=body) - -def doCreateOrg(): - cd = buildGAPIObject('directory') - name = getOrgUnitItem(sys.argv[3], pathOnly=True, absolutePath=False) - parent = '' - body = {} - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'description': - body['description'] = sys.argv[i+1].replace('\\n', '\n') - i += 2 - elif myarg == 'parent': - parent = getOrgUnitItem(sys.argv[i+1]) - i += 2 - elif myarg == 'noinherit': - body['blockInheritance'] = True - i += 1 - elif myarg == 'inherit': - body['blockInheritance'] = False - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam create org") - if parent.startswith('id:'): - parent = gapi.call(cd.orgunits(), 'get', - customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=parent, fields='orgUnitPath')['orgUnitPath'] - if parent == '/': - orgUnitPath = parent+name - else: - orgUnitPath = parent+'/'+name - if orgUnitPath.count('/') > 1: - body['parentOrgUnitPath'], body['name'] = orgUnitPath.rsplit('/', 1) - else: - body['parentOrgUnitPath'] = '/' - body['name'] = orgUnitPath[1:] - parent = body['parentOrgUnitPath'] - gapi.call(cd.orgunits(), 'insert', customerId=GC_Values[GC_CUSTOMER_ID], body=body, retry_reasons=[gapi.errors.ErrorReason.DAILY_LIMIT_EXCEEDED]) - print(f'Created OrgUnit {body["name"]}') - -def doUpdateUser(users, i): - cd = buildGAPIObject('directory') - if users is None: - users = [normalizeEmailAddressOrUID(sys.argv[3])] - body = getUserAttributes(i, cd, True) - vfe = 'primaryEmail' in body and body['primaryEmail'][:4].lower() == 'vfe@' - for user in users: - userKey = user - if vfe: - user_primary = gapi.call(cd.users(), 'get', userKey=userKey, fields='primaryEmail,id') - userKey = user_primary['id'] - user_primary = user_primary['primaryEmail'] - user_name, user_domain = splitEmailAddress(user_primary) - body['primaryEmail'] = f'vfe.{user_name}.{random.randint(1, 99999):05d}@{user_domain}' - body['emails'] = [{'type': 'custom', 'customType': 'former_employee', 'primary': False, 'address': user_primary}] - sys.stdout.write(f'updating user {user}...\n') - if body: - gapi.call(cd.users(), 'update', userKey=userKey, body=body) - -def doRemoveUsersAliases(users): - cd = buildGAPIObject('directory') - for user in users: - user_aliases = gapi.call(cd.users(), 'get', userKey=user, fields='aliases,id,primaryEmail') - user_id = user_aliases['id'] - user_primary = user_aliases['primaryEmail'] - if 'aliases' in user_aliases: - print(f'{user_primary} has {len(user_aliases["aliases"])} aliases') - for an_alias in user_aliases['aliases']: - print(f' removing alias {an_alias} for {user_primary}...') - gapi.call(cd.users().aliases(), 'delete', userKey=user_id, alias=an_alias) - else: - print(f'{user_primary} has no aliases') - -def deleteUserFromGroups(users): - cd = buildGAPIObject('directory') - for user in users: - user_groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', userKey=user, fields='groups(id,email)') - jcount = len(user_groups) - print(f'{user} is in {jcount} groups') - j = 0 - for user_group in user_groups: - j += 1 - print(f' removing {user} from {user_group["email"]}{currentCount(j, jcount)}') - gapi.call(cd.members(), 'delete', soft_errors=True, groupKey=user_group['id'], memberKey=user) - print('') - -def checkGroupExists(cd, group, i=0, count=0): - group = normalizeEmailAddressOrUID(group) - try: - return gapi.call(cd.groups(), 'get', - throw_reasons=gapi.errors.GROUP_GET_THROW_REASONS, retry_reasons=gapi.errors.GROUP_GET_RETRY_REASONS, - groupKey=group, fields='email')['email'] - except (gapi.errors.GapiGroupNotFoundError, gapi.errors.GapiDomainNotFoundError, gapi.errors.GapiDomainCannotUseApisError, gapi.errors.GapiForbiddenError, gapi.errors.GapiBadRequestError): - entityUnknownWarning('Group', group, i, count) - return None - -def _checkMemberRoleIsSuspended(member, validRoles, isSuspended): - if validRoles and member.get('role', ROLE_MEMBER) not in validRoles: - return False - if isSuspended is None: - return True - memberStatus = member.get('status', 'UNKNOWN') - if not isSuspended: - return memberStatus != 'SUSPENDED' - return memberStatus == 'SUSPENDED' - -UPDATE_GROUP_SUBCMDS = ['add', 'clear', 'delete', 'remove', 'sync', 'update'] -GROUP_ROLES_MAP = { - 'owner': ROLE_OWNER, 'owners': ROLE_OWNER, - 'manager': ROLE_MANAGER, 'managers': ROLE_MANAGER, - 'member': ROLE_MEMBER, 'members': ROLE_MEMBER, - } -MEMBER_DELIVERY_MAP = { - 'allmail': 'ALL_MAIL', 'digest': 'DIGEST', 'daily': 'DAILY', - 'abridged': 'DAILY', 'nomail': 'NONE', 'none': 'NONE' - } -def doUpdateGroup(): - -# Convert foo@googlemail.com to foo@gmail.com; eliminate periods in name for foo.bar@gmail.com - def _cleanConsumerAddress(emailAddress, mapCleanToOriginal): - atLoc = emailAddress.find('@') - if atLoc > 0: - if emailAddress[atLoc+1:] in ['gmail.com', 'googlemail.com']: - cleanEmailAddress = emailAddress[:atLoc].replace('.', '')+'@gmail.com' - if cleanEmailAddress != emailAddress: - mapCleanToOriginal[cleanEmailAddress] = emailAddress - return cleanEmailAddress - return emailAddress - - def _getRoleAndUsers(): - checkSuspended = None - role = None - delivery = None - i = 5 - if sys.argv[i].lower() in GROUP_ROLES_MAP: - role = GROUP_ROLES_MAP[sys.argv[i].lower()] - i += 1 - if sys.argv[i].lower() in ['suspended', 'notsuspended']: - checkSuspended = sys.argv[i].lower() == 'suspended' - i += 1 - if sys.argv[i].lower().replace('_', '') in MEMBER_DELIVERY_MAP: - delivery = MEMBER_DELIVERY_MAP[sys.argv[i].lower().replace('_', '')] - i += 1 - if sys.argv[i].lower() in usergroup_types: - users_email = getUsersToModify(entity_type=sys.argv[i].lower(), entity=sys.argv[i+1], checkSuspended=checkSuspended, groupUserMembersOnly=False) - else: - users_email = [normalizeEmailAddressOrUID(sys.argv[i], checkForCustomerId=True)] - return (role, users_email, delivery) - - gs_get_before_update = False - cd = buildGAPIObject('directory') - group = sys.argv[3] - myarg = sys.argv[4].lower() - items = [] - if myarg in UPDATE_GROUP_SUBCMDS: - group = normalizeEmailAddressOrUID(group) - if myarg == 'add': - role, users_email, delivery = _getRoleAndUsers() - if not role: - role = ROLE_MEMBER - if not checkGroupExists(cd, group): - return - if len(users_email) > 1: - sys.stderr.write(f'Group: {group}, Will add {len(users_email)} {role}s.\n') - for user_email in users_email: - item = ['gam', 'update', 'group', group, 'add', role] - if delivery: - item.append(delivery) - item.append(user_email) - items.append(item) - else: - body = {'role': role, 'email' if users_email[0].find('@') != -1 else 'id': users_email[0]} - add_text = [f'as {role}'] - if delivery: - body['delivery_settings'] = delivery - add_text.append(f'delivery {delivery}') - for i in range(2): - try: - gapi.call(cd.members(), 'insert', - throw_reasons=[gapi.errors.ErrorReason.DUPLICATE, gapi.errors.ErrorReason.MEMBER_NOT_FOUND, gapi.errors.ErrorReason.RESOURCE_NOT_FOUND, gapi.errors.ErrorReason.INVALID_MEMBER, gapi.errors.ErrorReason.CYCLIC_MEMBERSHIPS_NOT_ALLOWED], - groupKey=group, body=body) - print(f' Group: {group}, {users_email[0]} Added {" ".join(add_text)}') - break - except gapi.errors.GapiDuplicateError as e: - # check if user is a full member, not pending - try: - result = gapi.call(cd.members(), 'get', throw_reasons=[gapi.errors.ErrorReason.MEMBER_NOT_FOUND], memberKey=users_email[0], groupKey=group, fields='role') - print(f' Group: {group}, {users_email[0]} Add {" ".join(add_text)} Failed: Duplicate, already a {result["role"]}') - break # if get succeeds, user is a full member and we throw duplicate error - except gapi.errors.GapiMemberNotFoundError: - # insert fails on duplicate and get fails on not found, user is pending - print(f' Group: {group}, {users_email[0]} member is pending, deleting and re-adding to solve...') - gapi.call(cd.members(), 'delete', memberKey=users_email[0], groupKey=group) - continue # 2nd insert should succeed now that pending is clear - except (gapi.errors.GapiMemberNotFoundError, gapi.errors.GapiResourceNotFoundError, gapi.errors.GapiInvalidMemberError, gapi.errors.GapiCyclicMembershipsNotAllowedError) as e: - print(f' Group: {group}, {users_email[0]} Add {" ".join(add_text)} Failed: {str(e)}') - break - elif myarg == 'sync': - syncMembersSet = set() - syncMembersMap = {} - role, users_email, delivery = _getRoleAndUsers() - for user_email in users_email: - if user_email in ('*', GC_Values[GC_CUSTOMER_ID]): - syncMembersSet.add(GC_Values[GC_CUSTOMER_ID]) - else: - syncMembersSet.add(_cleanConsumerAddress(user_email.lower(), syncMembersMap)) - group = checkGroupExists(cd, group) - if group: - currentMembersSet = set() - currentMembersMap = {} - for current_email in getUsersToModify(entity_type='group', entity=group, member_type=role, groupUserMembersOnly=False): - if current_email == GC_Values[GC_CUSTOMER_ID]: - currentMembersSet.add(current_email) - else: - currentMembersSet.add(_cleanConsumerAddress(current_email.lower(), currentMembersMap)) -# Compare incoming members and current members using the cleaned addresses; we actually add/remove with the original addresses - to_add = [syncMembersMap.get(emailAddress, emailAddress) for emailAddress in syncMembersSet-currentMembersSet] - to_remove = [currentMembersMap.get(emailAddress, emailAddress) for emailAddress in currentMembersSet-syncMembersSet] - sys.stderr.write(f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n') - for user in to_add: - item = ['gam', 'update', 'group', group, 'add'] - if role: - item.append(role) - if delivery: - item.append(delivery) - item.append(user) - items.append(item) - for user in to_remove: - items.append(['gam', 'update', 'group', group, 'remove', user]) - elif myarg in ['delete', 'remove']: - _, users_email, _ = _getRoleAndUsers() - if not checkGroupExists(cd, group): - return - if len(users_email) > 1: - sys.stderr.write(f'Group: {group}, Will remove {len(users_email)} emails.\n') - for user_email in users_email: - items.append(['gam', 'update', 'group', group, 'remove', user_email]) - else: - try: - gapi.call(cd.members(), 'delete', - throw_reasons=[gapi.errors.ErrorReason.MEMBER_NOT_FOUND, gapi.errors.ErrorReason.INVALID_MEMBER], - groupKey=group, memberKey=users_email[0]) - print(f' Group: {group}, {users_email[0]} Removed') - except (gapi.errors.GapiMemberNotFoundError, gapi.errors.GapiInvalidMemberError) as e: - print(f' Group: {group}, {users_email[0]} Remove Failed: {str(e)}') - elif myarg == 'update': - role, users_email, delivery = _getRoleAndUsers() - group = checkGroupExists(cd, group) - if group: - if not role and not delivery: - role = ROLE_MEMBER - if len(users_email) > 1: - sys.stderr.write(f'Group: {group}, Will update {len(users_email)} {role}s.\n') - for user_email in users_email: - item = ['gam', 'update', 'group', group, 'update'] - if role: - item.append(role) - if delivery: - item.append(delivery) - item.append(user_email) - items.append(item) - else: - body = {} - update_text = [] - if role: - body['role'] = role - update_text.append(f'to {role}') - if delivery: - body['delivery_settings'] = delivery - update_text.append(f'delivery {delivery}') - try: - gapi.call(cd.members(), 'update', - throw_reasons=[gapi.errors.ErrorReason.MEMBER_NOT_FOUND, gapi.errors.ErrorReason.INVALID_MEMBER], - groupKey=group, memberKey=users_email[0], body=body) - print(f' Group: {group}, {users_email[0]} Updated {" ".join(update_text)}') - except (gapi.errors.GapiMemberNotFoundError, gapi.errors.GapiInvalidMemberError) as e: - print(f' Group: {group}, {users_email[0]} Update to {role} Failed: {str(e)}') - else: # clear - checkSuspended = None - fields = ['email', 'id'] - roles = [] - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg.upper() in [ROLE_OWNER, ROLE_MANAGER, ROLE_MEMBER]: - roles.append(myarg.upper()) - i += 1 - elif myarg in ['suspended', 'notsuspended']: - checkSuspended = myarg == 'suspended' - fields.append('status') - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam update group clear") - if roles: - roles = ','.join(sorted(set(roles))) - else: - roles = ROLE_MEMBER - group = normalizeEmailAddressOrUID(group) - member_type_message = f'{roles.lower()}s' - sys.stderr.write(f'Getting {member_type_message} of {group} (may take some time for large groups)...\n') - page_message = gapi.got_total_items_msg(f'{member_type_message}', '...') - validRoles, listRoles, listFields = _getRoleVerification(roles, f'nextPageToken,members({",".join(fields)})') - try: - result = gapi.get_all_pages(cd.members(), 'list', 'members', - page_message=page_message, - throw_reasons=gapi.errors.MEMBERS_THROW_REASONS, - groupKey=group, roles=listRoles, fields=listFields) - if not result: - print('Group already has 0 members') - return - users_email = [member.get('email', member['id']) for member in result if _checkMemberRoleIsSuspended(member, validRoles, checkSuspended)] - if len(users_email) > 1: - sys.stderr.write(f'Group: {group}, Will remove {len(users_email)} {"" if checkSuspended is None else ["Non-suspended ", "Suspended "][checkSuspended]}{roles}s.\n') - for user_email in users_email: - items.append(['gam', 'update', 'group', group, 'remove', user_email]) - else: - try: - gapi.call(cd.members(), 'delete', - throw_reasons=[gapi.errors.ErrorReason.MEMBER_NOT_FOUND, gapi.errors.ErrorReason.INVALID_MEMBER], - groupKey=group, memberKey=users_email[0]) - print(f' Group: {group}, {users_email[0]} Removed') - except (gapi.errors.GapiMemberNotFoundError, gapi.errors.GapiInvalidMemberError) as e: - print(f' Group: {group}, {users_email[0]} Remove Failed: {str(e)}') - except (gapi.errors.GapiGroupNotFoundError, gapi.errors.GapiDomainNotFoundError, gapi.errors.GapiInvalidError, gapi.errors.GapiForbiddenError): - entityUnknownWarning('Group', group, 0, 0) - if items: - run_batch(items) - else: - i = 4 - use_cd_api = False - gs = None - gs_body = {} - cd_body = {} - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'email': - use_cd_api = True - cd_body['email'] = normalizeEmailAddressOrUID(sys.argv[i+1]) - i += 2 - elif myarg == 'admincreated': - use_cd_api = True - cd_body['adminCreated'] = getBoolean(sys.argv[i+1], myarg) - i += 2 - elif myarg == 'getbeforeupdate': - gs_get_before_update = True - i += 1 - else: - if not gs: - gs = buildGAPIObject('groupssettings') - gs_object = gs._rootDesc - getGroupAttrValue(myarg, sys.argv[i+1], gs_object, gs_body, 'update') - i += 2 - group = normalizeEmailAddressOrUID(group) - if use_cd_api or (group.find('@') == -1): # group settings API won't take uid so we make sure cd API is used so that we can grab real email. - group = gapi.call(cd.groups(), 'update', groupKey=group, body=cd_body, fields='email')['email'] - if gs: - if not GroupIsAbuseOrPostmaster(group): - if gs_get_before_update: - current_settings = gapi.call(gs.groups(), 'get', - retry_reasons=[gapi.errors.ErrorReason.SERVICE_LIMIT], - groupUniqueId=group, fields='*') - if current_settings is not None: - gs_body = dict(list(current_settings.items()) + list(gs_body.items())) - if gs_body: - gapi.call(gs.groups(), 'update', retry_reasons=[gapi.errors.ErrorReason.SERVICE_LIMIT], groupUniqueId=group, body=gs_body) - print(f'updated group {group}') - -def doUpdateAlias(): - cd = buildGAPIObject('directory') - alias = normalizeEmailAddressOrUID(sys.argv[3], noUid=True, noLower=True) - target_type = sys.argv[4].lower() - if target_type not in ['user', 'group', 'target']: - controlflow.expected_argument_exit("target type", ", ".join(['user', 'group', 'target']), target_type) - target_email = normalizeEmailAddressOrUID(sys.argv[5]) - try: - gapi.call(cd.users().aliases(), 'delete', throw_reasons=[gapi.errors.ErrorReason.INVALID], userKey=alias, alias=alias) - except gapi.errors.GapiInvalidError: - gapi.call(cd.groups().aliases(), 'delete', groupKey=alias, alias=alias) - if target_type == 'user': - gapi.call(cd.users().aliases(), 'insert', userKey=target_email, body={'alias': alias}) - elif target_type == 'group': - gapi.call(cd.groups().aliases(), 'insert', groupKey=target_email, body={'alias': alias}) - elif target_type == 'target': - try: - gapi.call(cd.users().aliases(), 'insert', throw_reasons=[gapi.errors.ErrorReason.INVALID], userKey=target_email, body={'alias': alias}) - except gapi.errors.GapiInvalidError: - gapi.call(cd.groups().aliases(), 'insert', groupKey=target_email, body={'alias': alias}) - print(f'updated alias {alias}') - -def doUpdateMobile(): - cd = buildGAPIObject('directory') - resourceIds = sys.argv[3] - match_users = None - doit = False - if resourceIds[:6] == 'query:': - query = resourceIds[6:] - fields = 'nextPageToken,mobiledevices(resourceId,email)' - page_message = gapi.got_total_items_msg('Mobile Devices', '...\n') - devices = gapi.get_all_pages(cd.mobiledevices(), 'list', - page_message=page_message, - customerId=GC_Values[GC_CUSTOMER_ID], - items='mobiledevices', query=query, fields=fields) - else: - devices = [{'resourceId': resourceIds, 'email': ['not set']}] - doit = True - i = 4 - body = {} - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'action': - body['action'] = sys.argv[i+1].lower() - validActions = ['wipe', 'wipeaccount', 'accountwipe', 'wipe_account', 'account_wipe', 'approve', 'block', 'cancel_remote_wipe_then_activate', 'cancel_remote_wipe_then_block'] - if body['action'] not in validActions: - controlflow.expected_argument_exit("action", ", ".join(validActions), body['action']) - if body['action'] == 'wipe': - body['action'] = 'admin_remote_wipe' - elif body['action'].replace('_', '') in ['accountwipe', 'wipeaccount']: - body['action'] = 'admin_account_wipe' - i += 2 - elif myarg in ['ifusers', 'matchusers']: - match_users = getUsersToModify(entity_type=sys.argv[i+1].lower(), entity=sys.argv[i+2]) - i += 3 - elif myarg == 'doit': - doit = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam update mobile") - if body: - if doit: - print(f'Updating {len(devices)} devices') - describe_as = 'Performing' - else: - print(f'Showing {len(devices)} changes that would be made, not actually making changes because doit argument not specified') - describe_as = 'Would perform' - for device in devices: - device_user = device.get('email', [''])[0] - if match_users and device_user not in match_users: - print(f'Skipping device for user {device_user} that did not match match_users argument') - else: - print(f'{describe_as} {body["action"]} on user {device_user} device {device["resourceId"]}') - if doit: - gapi.call(cd.mobiledevices(), 'action', resourceId=device['resourceId'], body=body, customerId=GC_Values[GC_CUSTOMER_ID]) - -def doDeleteMobile(): - cd = buildGAPIObject('directory') - resourceId = sys.argv[3] - gapi.call(cd.mobiledevices(), 'delete', resourceId=resourceId, customerId=GC_Values[GC_CUSTOMER_ID]) - -def doUpdateOrg(): - cd = buildGAPIObject('directory') - orgUnitPath = getOrgUnitItem(sys.argv[3]) - if sys.argv[4].lower() in ['move', 'add']: - entity_type = sys.argv[5].lower() - if entity_type in usergroup_types: - users = getUsersToModify(entity_type=entity_type, entity=sys.argv[6]) - else: - entity_type = 'users' - users = getUsersToModify(entity_type=entity_type, entity=sys.argv[5]) - if (entity_type.startswith('cros')) or ((entity_type == 'all') and (sys.argv[6].lower() == 'cros')): - for l in range(0, len(users), 50): - move_body = {'deviceIds': users[l:l+50]} - print(f' moving {len(move_body["deviceIds"])} devices to {orgUnitPath}') - gapi.call(cd.chromeosdevices(), 'moveDevicesToOu', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=orgUnitPath, body=move_body) - else: - i = 0 - count = len(users) - for user in users: - i += 1 - sys.stderr.write(f' moving {user} to {orgUnitPath}{currentCountNL(i, count)}') - try: - gapi.call(cd.users(), 'update', throw_reasons=[gapi.errors.ErrorReason.CONDITION_NOT_MET], userKey=user, body={'orgUnitPath': orgUnitPath}) - except gapi.errors.GapiConditionNotMetError: - pass - else: - body = {} - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'name': - body['name'] = sys.argv[i+1] - i += 2 - elif myarg == 'description': - body['description'] = sys.argv[i+1].replace('\\n', '\n') - i += 2 - elif myarg == 'parent': - parent = getOrgUnitItem(sys.argv[i+1]) - if parent.startswith('id:'): - body['parentOrgUnitId'] = parent - else: - body['parentOrgUnitPath'] = parent - i += 2 - elif myarg == 'noinherit': - body['blockInheritance'] = True - i += 1 - elif myarg == 'inherit': - body['blockInheritance'] = False - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam update org") - gapi.call(cd.orgunits(), 'update', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)), body=body) - -def doWhatIs(): - cd = buildGAPIObject('directory') - email = normalizeEmailAddressOrUID(sys.argv[2]) - try: - user_or_alias = gapi.call(cd.users(), 'get', throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND, gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.INVALID], userKey=email, fields='id,primaryEmail') - if (user_or_alias['primaryEmail'].lower() == email) or (user_or_alias['id'] == email): - sys.stderr.write(f'{email} is a user\n\n') - doGetUserInfo(user_email=email) - return - sys.stderr.write(f'{email} is a user alias\n\n') - doGetAliasInfo(alias_email=email) - return - except (gapi.errors.GapiNotFoundError, gapi.errors.GapiBadRequestError, gapi.errors.GapiInvalidError): - sys.stderr.write(f'{email} is not a user...\n') - sys.stderr.write(f'{email} is is not a user alias...\n') - try: - group = gapi.call(cd.groups(), 'get', throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND, gapi.errors.ErrorReason.BAD_REQUEST], groupKey=email, fields='id,email') - except (gapi.errors.GapiNotFoundError, gapi.errors.GapiBadRequestError): - controlflow.system_error_exit(1, f'{email} is not a group either!\n\nDoesn\'t seem to exist!\n\n') - if (group['email'].lower() == email) or (group['id'] == email): - sys.stderr.write(f'{email} is a group\n\n') - doGetGroupInfo(group_name=email) - else: - sys.stderr.write(f'{email} is a group alias\n\n') - doGetAliasInfo(alias_email=email) - -def convertSKU2ProductId(res, sku, customerId): - results = gapi.call(res.subscriptions(), 'list', customerId=customerId) - for subscription in results['subscriptions']: - if sku == subscription['skuId']: - return subscription['subscriptionId'] - controlflow.system_error_exit(3, f'could not find subscription for customer {customerId} and SKU {sku}') - -def doDeleteResoldSubscription(): - res = buildGAPIObject('reseller') - customerId = sys.argv[3] - sku = sys.argv[4] - deletionType = sys.argv[5] - subscriptionId = convertSKU2ProductId(res, sku, customerId) - gapi.call(res.subscriptions(), 'delete', customerId=customerId, subscriptionId=subscriptionId, deletionType=deletionType) - print(f'Cancelled {sku} for {customerId}') - -def doCreateResoldSubscription(): - res = buildGAPIObject('reseller') - customerId = sys.argv[3] - customerAuthToken, body = _getResoldSubscriptionAttr(sys.argv[4:], customerId) - result = gapi.call(res.subscriptions(), 'insert', customerId=customerId, customerAuthToken=customerAuthToken, body=body, fields='customerId') - print('Created subscription:') - display.print_json(result) - -def doUpdateResoldSubscription(): - res = buildGAPIObject('reseller') - function = None - customerId = sys.argv[3] - sku = sys.argv[4] - subscriptionId = convertSKU2ProductId(res, sku, customerId) - kwargs = {} - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'activate': - function = 'activate' - i += 1 - elif myarg == 'suspend': - function = 'suspend' - i += 1 - elif myarg == 'startpaidservice': - function = 'startPaidService' - i += 1 - elif myarg in ['renewal', 'renewaltype']: - function = 'changeRenewalSettings' - kwargs['body'] = {'renewalType': sys.argv[i+1].upper()} - i += 2 - elif myarg in ['seats']: - function = 'changeSeats' - kwargs['body'] = {'numberOfSeats': getInteger(sys.argv[i+1], 'numberOfSeats', minVal=0)} - if len(sys.argv) > i + 2 and sys.argv[i+2].isdigit(): - kwargs['body']['maximumNumberOfSeats'] = getInteger(sys.argv[i+2], 'maximumNumberOfSeats', minVal=0) - i += 3 - else: - i += 2 - elif myarg in ['plan']: - function = 'changePlan' - kwargs['body'] = {'planName': sys.argv[i+1].upper()} - i += 2 - while i < len(sys.argv): - planarg = sys.argv[i].lower() - if planarg == 'seats': - kwargs['body']['seats'] = {'numberOfSeats': getInteger(sys.argv[i+1], 'numberOfSeats', minVal=0)} - if len(sys.argv) > i + 2 and sys.argv[i+2].isdigit(): - kwargs['body']['seats']['maximumNumberOfSeats'] = getInteger(sys.argv[i+2], 'maximumNumberOfSeats', minVal=0) - i += 3 - else: - i += 2 - elif planarg in ['purchaseorderid', 'po']: - kwargs['body']['purchaseOrderId'] = sys.argv[i+1] - i += 2 - elif planarg in ['dealcode', 'deal']: - kwargs['body']['dealCode'] = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(planarg, "gam update resoldsubscription plan") - else: - controlflow.invalid_argument_exit(myarg, "gam update resoldsubscription") - result = gapi.call(res.subscriptions(), function, customerId=customerId, subscriptionId=subscriptionId, **kwargs) - print(f'Updated {customerId} SKU {sku} subscription:') - if result: - display.print_json(result) - -def doGetResoldSubscriptions(): - res = buildGAPIObject('reseller') - customerId = sys.argv[3] - customerAuthToken = None - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg in ['customerauthtoken', 'transfertoken']: - customerAuthToken = sys.argv[i+1] - i += 2 - else: - controlflow.invalid_argument_exit(myarg, "gam info resoldsubscriptions") - result = gapi.call(res.subscriptions(), 'list', customerId=customerId, customerAuthToken=customerAuthToken) - display.print_json(result) - -def _getResoldSubscriptionAttr(arg, customerId): - body = {'plan': {}, - 'seats': {}, - 'customerId': customerId} - customerAuthToken = None - i = 0 - while i < len(arg): - myarg = arg[i].lower().replace('_', '') - if myarg in ['deal', 'dealcode']: - body['dealCode'] = arg[i+1] - elif myarg in ['plan', 'planname']: - body['plan']['planName'] = arg[i+1].upper() - elif myarg in ['purchaseorderid', 'po']: - body['purchaseOrderId'] = arg[i+1] - elif myarg in ['seats']: - body['seats']['numberOfSeats'] = getInteger(sys.argv[i+1], 'numberOfSeats', minVal=0) - if len(arg) > i + 2 and arg[i+2].isdigit(): - body['seats']['maximumNumberOfSeats'] = getInteger(sys.argv[i+2], 'maximumNumberOfSeats', minVal=0) - i += 1 - elif myarg in ['sku', 'skuid']: - _, body['skuId'] = getProductAndSKU(arg[i+1]) - elif myarg in ['customerauthtoken', 'transfertoken']: - customerAuthToken = arg[i+1] - else: - controlflow.invalid_argument_exit(myarg, "gam create resoldsubscription") - i += 2 - return customerAuthToken, body - -def doGetResoldCustomer(): - res = buildGAPIObject('reseller') - customerId = sys.argv[3] - result = gapi.call(res.customers(), 'get', customerId=customerId) - display.print_json(result) - -def _getResoldCustomerAttr(arg): - body = {} - customerAuthToken = None - i = 0 - while i < len(arg): - myarg = arg[i].lower().replace('_', '') - if myarg in ADDRESS_FIELDS_ARGUMENT_MAP: - body.setdefault('postalAddress', {}) - body['postalAddress'][ADDRESS_FIELDS_ARGUMENT_MAP[myarg]] = arg[i+1] - elif myarg in ['email', 'alternateemail']: - body['alternateEmail'] = arg[i+1] - elif myarg in ['phone', 'phonenumber']: - body['phoneNumber'] = arg[i+1] - elif myarg in ['customerauthtoken', 'transfertoken']: - customerAuthToken = arg[i+1] - else: - controlflow.invalid_argument_exit(myarg, f"gam {sys.argv[1]} resoldcustomer") - i += 2 - return customerAuthToken, body - -def doUpdateResoldCustomer(): - res = buildGAPIObject('reseller') - customerId = sys.argv[3] - customerAuthToken, body = _getResoldCustomerAttr(sys.argv[4:]) - gapi.call(res.customers(), 'patch', customerId=customerId, body=body, customerAuthToken=customerAuthToken, fields='customerId') - print(f'updated customer {customerId}') - -def doCreateResoldCustomer(): - res = buildGAPIObject('reseller') - customerAuthToken, body = _getResoldCustomerAttr(sys.argv[4:]) - body['customerDomain'] = sys.argv[3] - result = gapi.call(res.customers(), 'insert', body=body, customerAuthToken=customerAuthToken, fields='customerId,customerDomain') - print(f'Created customer {result["customerDomain"]} with id {result["customerId"]}') - -def _getValueFromOAuth(field, credentials=None): - if not credentials: - credentials = auth.get_admin_credentials() - return credentials.get_token_value(field) - -def doGetMemberInfo(): - cd = buildGAPIObject('directory') - memberKey = normalizeEmailAddressOrUID(sys.argv[3]) - groupKey = normalizeEmailAddressOrUID(sys.argv[4]) - info = gapi.call(cd.members(), 'get', memberKey=memberKey, groupKey=groupKey) - display.print_json(info) - -def doGetUserInfo(user_email=None): - - def user_lic_result(request_id, response, exception): - if response and 'skuId' in response: - user_licenses.append(response['skuId']) - - cd = buildGAPIObject('directory') - i = 3 - if user_email is None: - if len(sys.argv) > 3: - user_email = normalizeEmailAddressOrUID(sys.argv[3]) - i = 4 - else: - user_email = _getValueFromOAuth('email') - getSchemas = getAliases = getGroups = getLicenses = True - projection = 'full' - customFieldMask = viewType = None - skus = sorted(SKUS) - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'noaliases': - getAliases = False - i += 1 - elif myarg == 'nogroups': - getGroups = False - i += 1 - elif myarg in ['nolicenses', 'nolicences']: - getLicenses = False - i += 1 - elif myarg in ['sku', 'skus']: - skus = sys.argv[i+1].split(',') - i += 2 - elif myarg == 'noschemas': - getSchemas = False - projection = 'basic' - i += 1 - elif myarg in ['custom', 'schemas']: - getSchemas = True - projection = 'custom' - customFieldMask = sys.argv[i+1] - i += 2 - elif myarg == 'userview': - viewType = 'domain_public' - getGroups = getLicenses = False - i += 1 - elif myarg in ['nousers', 'groups']: - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam info user") - user = gapi.call(cd.users(), 'get', userKey=user_email, projection=projection, customFieldMask=customFieldMask, viewType=viewType) - print(f'User: {user["primaryEmail"]}') - if 'name' in user and 'givenName' in user['name']: - print(f'First Name: {user["name"]["givenName"]}') - if 'name' in user and 'familyName' in user['name']: - print(f'Last Name: {user["name"]["familyName"]}') - if 'languages' in user: - up = 'languageCode' - languages = [row[up] for row in user['languages'] if up in row] - if languages: - print(f'Languages: {",".join(languages)}') - up = 'customLanguage' - languages = [row[up] for row in user['languages'] if up in row] - if languages: - print(f'Custom Languages: {",".join(languages)}') - if 'isAdmin' in user: - print(f'Is a Super Admin: {user["isAdmin"]}') - if 'isDelegatedAdmin' in user: - print(f'Is Delegated Admin: {user["isDelegatedAdmin"]}') - if 'isEnrolledIn2Sv' in user: - print(f'2-step enrolled: {user["isEnrolledIn2Sv"]}') - if 'isEnforcedIn2Sv' in user: - print(f'2-step enforced: {user["isEnforcedIn2Sv"]}') - if 'agreedToTerms' in user: - print(f'Has Agreed to Terms: {user["agreedToTerms"]}') - if 'ipWhitelisted' in user: - print(f'IP Whitelisted: {user["ipWhitelisted"]}') - if 'suspended' in user: - print(f'Account Suspended: {user["suspended"]}') - if 'suspensionReason' in user: - print(f'Suspension Reason: {user["suspensionReason"]}') - if 'archived' in user: - print(f'Is Archived: {user["archived"]}') - if 'changePasswordAtNextLogin' in user: - print(f'Must Change Password: {user["changePasswordAtNextLogin"]}') - if 'id' in user: - print(f'Google Unique ID: {user["id"]}') - if 'customerId' in user: - print(f'Customer ID: {user["customerId"]}') - if 'isMailboxSetup' in user: - print(f'Mailbox is setup: {user["isMailboxSetup"]}') - if 'includeInGlobalAddressList' in user: - print(f'Included in GAL: {user["includeInGlobalAddressList"]}') - if 'creationTime' in user: - print(f'Creation Time: {user["creationTime"]}') - if 'lastLoginTime' in user: - if user['lastLoginTime'] == NEVER_TIME: - print('Last login time: Never') - else: - print(f'Last login time: {user["lastLoginTime"]}') - if 'orgUnitPath' in user: - print(f'Google Org Unit Path: {user["orgUnitPath"]}') - if 'thumbnailPhotoUrl' in user: - print(f'Photo URL: {user["thumbnailPhotoUrl"]}') - if 'recoveryPhone' in user: - print(f'Recovery Phone: {user["recoveryPhone"]}') - if 'recoveryEmail' in user: - print(f'Recovery Email: {user["recoveryEmail"]}') - if 'notes' in user: - print('Notes:') - notes = user['notes'] - if isinstance(notes, dict): - contentType = notes.get('contentType', 'text_plain') - print(f' contentType: {contentType}') - if contentType == 'text_html': - print(utils.indentMultiLineText(f' value: {utils.dehtml(notes["value"])}', n=2)) - else: - print(utils.indentMultiLineText(f' value: {notes["value"]}', n=2)) - else: - print(utils.indentMultiLineText(f' value: {notes}', n=2)) - print('') - if 'gender' in user: - print('Gender') - gender = user['gender'] - for key in gender: - if key == 'customGender' and not gender[key]: - continue - print(f' {key}: {gender[key]}') - print('') - if 'keywords' in user: - print('Keywords:') - for keyword in user['keywords']: - for key in keyword: - if key == 'customType' and not keyword[key]: - continue - print(f' {key}: {keyword[key]}') - print('') - if 'ims' in user: - print('IMs:') - for im in user['ims']: - for key in im: - print(f' {key}: {im[key]}') - print('') - if 'addresses' in user: - print('Addresses:') - for address in user['addresses']: - for key in address: - if key != 'formatted': - print(f' {key}: {address[key]}') - else: - addr = address[key].replace("\n", "\\n") - print(f' {key}: {addr}') - print('') - if 'organizations' in user: - print('Organizations:') - for org in user['organizations']: - for key in org: - if key == 'customType' and not org[key]: - continue - print(f' {key}: {org[key]}') - print('') - if 'locations' in user: - print('Locations:') - for location in user['locations']: - for key in location: - if key == 'customType' and not location[key]: - continue - print(f' {key}: {location[key]}') - print('') - if 'sshPublicKeys' in user: - print('SSH Public Keys:') - for sshkey in user['sshPublicKeys']: - for key in sshkey: - print(f' {key}: {sshkey[key]}') - print('') - if 'posixAccounts' in user: - print('Posix Accounts:') - for posix in user['posixAccounts']: - for key in posix: - print(f' {key}: {posix[key]}') - print('') - if 'phones' in user: - print('Phones:') - for phone in user['phones']: - for key in phone: - print(f' {key}: {phone[key]}') - print('') - if 'emails' in user: - if len(user['emails']) > 1: - print('Other Emails:') - for an_email in user['emails']: - if an_email['address'].lower() == user['primaryEmail'].lower(): - continue - for key in an_email: - if key == 'type' and an_email[key] == 'custom': - continue - if key == 'customType': - print(f' type: {an_email[key]}') - else: - print(f' {key}: {an_email[key]}') - print('') - if 'relations' in user: - print('Relations:') - for relation in user['relations']: - for key in relation: - if key == 'type' and relation[key] == 'custom': - continue - if key == 'customType': - print(f' type: {relation[key]}') - else: - print(f' {key}: {relation[key]}') - print('') - if 'externalIds' in user: - print('External IDs:') - for externalId in user['externalIds']: - for key in externalId: - if key == 'type' and externalId[key] == 'custom': - continue - if key == 'customType': - print(f' typw: {externalId[key]}') - else: - print(f' {key}: {externalId[key]}') - print('') - if 'websites' in user: - print('Websites:') - for website in user['websites']: - for key in website: - if key == 'type' and website[key] == 'custom': - continue - if key == 'customType': - print(f' type: {website[key]}') - else: - print(f' {key}: {website[key]}') - print('') - if getSchemas: - if 'customSchemas' in user: - print('Custom Schemas:') - for schema in user['customSchemas']: - print(f' Schema: {schema}') - for field in user['customSchemas'][schema]: - if isinstance(user['customSchemas'][schema][field], list): - print(f' {field}:') - for an_item in user['customSchemas'][schema][field]: - print(f' type: {an_item["type"]}') - if an_item['type'] == 'custom': - print(f' customType: {an_item["customType"]}') - print(f' value: {an_item["value"]}') - else: - print(f' {field}: {user["customSchemas"][schema][field]}') - print() - if getAliases: - if 'aliases' in user: - print('Email Aliases:') - for alias in user['aliases']: - print(f' {alias}') - if 'nonEditableAliases' in user: - print('Non-Editable Aliases:') - for alias in user['nonEditableAliases']: - print(f' {alias}') - if getGroups: - groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', userKey=user_email, fields='groups(name,email),nextPageToken') - if groups: - print(f'Groups: ({len(groups)})') - for group in groups: - print(f' {group["name"]} <{group["email"]}>') - if getLicenses: - print('Licenses:') - lic = buildGAPIObject('licensing') - lbatch = lic.new_batch_http_request(callback=user_lic_result) - user_licenses = [] - for sku in skus: - productId, skuId = getProductAndSKU(sku) - lbatch.add(lic.licenseAssignments().get(userId=user_email, productId=productId, skuId=skuId, fields='skuId')) - lbatch.execute() - for user_license in user_licenses: - print(f' {_formatSKUIdDisplayName(user_license)}') - -def _skuIdToDisplayName(skuId): - return SKUS[skuId]['displayName'] if skuId in SKUS else skuId - -def _formatSKUIdDisplayName(skuId): - skuIdDisplay = _skuIdToDisplayName(skuId) - if skuId == skuIdDisplay: - return skuId - return f'{skuId} ({skuIdDisplay})' - -def doGetGroupInfo(group_name=None): - cd = buildGAPIObject('directory') - gs = buildGAPIObject('groupssettings') - getAliases = getUsers = True - getGroups = False - if group_name is None: - group_name = normalizeEmailAddressOrUID(sys.argv[3]) - i = 4 - else: - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'nousers': - getUsers = False - i += 1 - elif myarg == 'noaliases': - getAliases = False - i += 1 - elif myarg == 'groups': - getGroups = True - i += 1 - elif myarg in ['nogroups', 'nolicenses', 'nolicences', 'noschemas', 'schemas', 'userview']: - i += 1 - if myarg == 'schemas': - i += 1 - else: - controlflow.invalid_argument_exit(myarg, "gam info group") - basic_info = gapi.call(cd.groups(), 'get', groupKey=group_name) - settings = {} - if not GroupIsAbuseOrPostmaster(basic_info['email']): - try: - settings = gapi.call(gs.groups(), 'get', throw_reasons=[gapi.errors.ErrorReason.AUTH_ERROR], retry_reasons=[gapi.errors.ErrorReason.SERVICE_LIMIT], - groupUniqueId=basic_info['email']) # Use email address retrieved from cd since GS API doesn't support uid - if settings is None: - settings = {} - except gapi.errors.GapiAuthErrorError: - pass - print('') - print('Group Settings:') - for key, value in list(basic_info.items()): - if (key in ['kind', 'etag']) or ((key == 'aliases') and (not getAliases)): - continue - if isinstance(value, list): - print(f' {key}:') - for val in value: - print(f' {val}') - else: - print(f' {key}: {value}') - for key, value in list(settings.items()): - if key in ['kind', 'etag', 'description', 'email', 'name']: - continue - print(f' {key}: {value}') - if getGroups: - groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', - userKey=basic_info['email'], fields='nextPageToken,groups(name,email)') - if groups: - print(f'Groups: ({len(groups)})') - for groupm in groups: - print(f' {groupm["name"]}: {groupm["email"]}') - if getUsers: - members = gapi.get_all_pages(cd.members(), 'list', 'members', groupKey=group_name, fields='nextPageToken,members(email,id,role,type)') - print('Members:') - for member in members: - print(f' {member.get("role", ROLE_MEMBER).lower()}: {member.get("email", member["id"])} ({member["type"].lower()})') - print(f'Total {len(members)} users in group') - -def doGetAliasInfo(alias_email=None): - cd = buildGAPIObject('directory') - if alias_email is None: - alias_email = normalizeEmailAddressOrUID(sys.argv[3]) - try: - result = gapi.call(cd.users(), 'get', throw_reasons=[gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.BAD_REQUEST], userKey=alias_email) - except (gapi.errors.GapiInvalidError, gapi.errors.GapiBadRequestError): - result = gapi.call(cd.groups(), 'get', groupKey=alias_email) - print(f' Alias Email: {alias_email}') - try: - if result['primaryEmail'].lower() == alias_email.lower(): - controlflow.system_error_exit(3, f'{alias_email} is a primary user email address, not an alias.') - print(f' User Email: {result["primaryEmail"]}') - except KeyError: - print(f' Group Email: {result["email"]}') - print(f' Unique ID: {result["id"]}') - -def doGetMobileInfo(): - cd = buildGAPIObject('directory') - resourceId = sys.argv[3] - info = gapi.call(cd.mobiledevices(), 'get', customerId=GC_Values[GC_CUSTOMER_ID], resourceId=resourceId) - if 'deviceId' in info: - info['deviceId'] = info['deviceId'].encode('unicode-escape').decode(UTF8) - attrib = 'securityPatchLevel' - if attrib in info and int(info[attrib]): - info[attrib] = utils.formatTimestampYMDHMS(info[attrib]) - display.print_json(info) - -def doSiteVerifyShow(): - verif = buildGAPIObject('siteVerification') - a_domain = sys.argv[3] - txt_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'INET_DOMAIN', 'identifier':a_domain}, 'verificationMethod':'DNS_TXT'}) - print(f'TXT Record Name: {a_domain}') - print(f'TXT Record Value: {txt_record["token"]}') - print() - cname_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'INET_DOMAIN', 'identifier':a_domain}, 'verificationMethod':'DNS_CNAME'}) - cname_token = cname_record['token'] - cname_list = cname_token.split(' ') - cname_subdomain = cname_list[0] - cname_value = cname_list[1] - print(f'CNAME Record Name: {cname_subdomain}.{a_domain}') - print(f'CNAME Record Value: {cname_value}') - print('') - webserver_file_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'SITE', 'identifier':f'http://{a_domain}/'}, 'verificationMethod':'FILE'}) - webserver_file_token = webserver_file_record['token'] - print(f'Saving web server verification file to: {webserver_file_token}') - fileutils.write_file(webserver_file_token, f'google-site-verification: {webserver_file_token}', continue_on_error=True) - print(f'Verification File URL: http://{a_domain}/{webserver_file_token}') - print() - webserver_meta_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'SITE', 'identifier':f'http://{a_domain}/'}, 'verificationMethod':'META'}) - print(f'Meta URL: http://{a_domain}/') - print(f'Meta HTML Header Data: {webserver_meta_record["token"]}') - print() - -def doGetSiteVerifications(): - verif = buildGAPIObject('siteVerification') - sites = gapi.get_items(verif.webResource(), 'list', 'items') - if sites: - for site in sites: - print(f'Site: {site["site"]["identifier"]}') - print(f'Type: {site["site"]["type"]}') - print('Owners:') - for owner in site['owners']: - print(f' {owner}') - print() - else: - print('No Sites Verified.') - -def doSiteVerifyAttempt(): - verif = buildGAPIObject('siteVerification') - a_domain = sys.argv[3] - verificationMethod = sys.argv[4].upper() - if verificationMethod == 'CNAME': - verificationMethod = 'DNS_CNAME' - elif verificationMethod in ['TXT', 'TEXT']: - verificationMethod = 'DNS_TXT' - if verificationMethod in ['DNS_TXT', 'DNS_CNAME']: - verify_type = 'INET_DOMAIN' - identifier = a_domain - else: - verify_type = 'SITE' - identifier = f'http://{a_domain}/' - body = {'site':{'type':verify_type, 'identifier':identifier}, 'verificationMethod':verificationMethod} - try: - verify_result = gapi.call(verif.webResource(), 'insert', throw_reasons=[gapi.errors.ErrorReason.BAD_REQUEST], verificationMethod=verificationMethod, body=body) - except gapi.errors.GapiBadRequestError as e: - print(f'ERROR: {str(e)}') - verify_data = gapi.call(verif.webResource(), 'getToken', body=body) - print(f'Method: {verify_data["method"]}') - print(f'Expected Token: {verify_data["token"]}') - if verify_data['method'] in ['DNS_CNAME', 'DNS_TXT']: - simplehttp = transport.create_http() - base_url = 'https://dns.google/resolve?' - query_params = {} - if verify_data['method'] == 'DNS_CNAME': - cname_token = verify_data['token'] - cname_list = cname_token.split(' ') - cname_subdomain = cname_list[0] - query_params['name'] = f'{cname_subdomain}.{a_domain}' - query_params['type'] = 'cname' - else: - query_params['name'] = a_domain - query_params['type'] = 'txt' - full_url = base_url + urlencode(query_params) - (_, c) = simplehttp.request(full_url, 'GET') - result = json.loads(c) - status = result['Status'] - if status == 0 and 'Answer' in result: - answers = result['Answer'] - if verify_data['method'] == 'DNS_CNAME': - answer = answers[0]['data'] - else: - answer = 'no matching record found' - for possible_answer in answers: - possible_answer['data'] = possible_answer['data'].strip('"') - if possible_answer['data'].startswith('google-site-verification'): - answer = possible_answer['data'] - break - print(f'Unrelated TXT record: {possible_answer["data"]}') - print(f'Found DNS Record: {answer}') - elif status == 0: - controlflow.system_error_exit(1, 'DNS record not found') - else: - controlflow.system_error_exit(status, DNS_ERROR_CODES_MAP.get(status, f'Unknown error {status}')) - return - print('SUCCESS!') - print(f'Verified: {verify_result["site"]["identifier"]}') - print(f'ID: {verify_result["id"]}') - print(f'Type: {verify_result["site"]["type"]}') - print('All Owners:') - try: - for owner in verify_result['owners']: - print(f' {owner}') - except KeyError: - pass - print() - print(f'You can now add {a_domain} or it\'s subdomains as secondary or domain aliases of the {GC_Values[GC_DOMAIN]} G Suite Account.') - -def orgUnitPathQuery(path, checkSuspended): - query = "orgUnitPath='{0}'".format(path.replace("'", "\\'")) if path != '/' else '' - if checkSuspended is not None: - query += f' isSuspended={checkSuspended}' - return query - -def makeOrgUnitPathAbsolute(path): - if path == '/': - return path - if path.startswith('/'): - return path.rstrip('/') - if path.startswith('id:'): - return path - if path.startswith('uid:'): - return path[1:] - return '/'+path.rstrip('/') - -def makeOrgUnitPathRelative(path): - if path == '/': - return path - if path.startswith('/'): - return path[1:].rstrip('/') - if path.startswith('id:'): - return path - if path.startswith('uid:'): - return path[1:] - return path.rstrip('/') - -def encodeOrgUnitPath(path): - if path.find('+') == -1 and path.find('%') == -1: - return path - encpath = '' - for c in path: - if c == '+': - encpath += '%2B' - elif c == '%': - encpath += '%25' - else: - encpath += c - return encpath - -def getOrgUnitItem(orgUnit, pathOnly=False, absolutePath=True): - if pathOnly and (orgUnit.startswith('id:') or orgUnit.startswith('uid:')): - controlflow.system_error_exit(2, f'{orgUnit} is not valid in this context') - if absolutePath: - return makeOrgUnitPathAbsolute(orgUnit) - return makeOrgUnitPathRelative(orgUnit) - -def getTopLevelOrgId(cd, orgUnitPath): - try: - # create a temp org so we can learn what the top level org ID is (sigh) - temp_org = gapi.call(cd.orgunits(), 'insert', customerId=GC_Values[GC_CUSTOMER_ID], - body={'name': 'temp-delete-me', 'parentOrgUnitPath': orgUnitPath}, - fields='parentOrgUnitId,orgUnitId') - gapi.call(cd.orgunits(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=temp_org['orgUnitId']) - return temp_org['parentOrgUnitId'] - except: - pass - return None - -def getOrgUnitId(orgUnit, cd=None): - if cd is None: - cd = buildGAPIObject('directory') - orgUnit = getOrgUnitItem(orgUnit) - if orgUnit[:3] == 'id:': - return (orgUnit, orgUnit) - if orgUnit == '/': - result = gapi.call(cd.orgunits(), 'list', - customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath='/', type='children', - fields='organizationUnits(parentOrgUnitId)') - if result.get('organizationUnits', []): - return (orgUnit, result['organizationUnits'][0]['parentOrgUnitId']) - topLevelOrgId = getTopLevelOrgId(cd, '/') - if topLevelOrgId: - return (orgUnit, topLevelOrgId) - return (orgUnit, '/') #Bogus but should never happen - result = gapi.call(cd.orgunits(), 'get', - customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnit)), fields='orgUnitId') - return (orgUnit, result['orgUnitId']) - -def doGetOrgInfo(name=None, return_attrib=None): - cd = buildGAPIObject('directory') - checkSuspended = None - if not name: - name = getOrgUnitItem(sys.argv[3]) - get_users = True - show_children = False - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'nousers': - get_users = False - i += 1 - elif myarg in ['children', 'child']: - show_children = True - i += 1 - elif myarg in ['suspended', 'notsuspended']: - checkSuspended = myarg == 'suspended' - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam info org") - if name == '/': - orgs = gapi.call(cd.orgunits(), 'list', - customerId=GC_Values[GC_CUSTOMER_ID], type='children', - fields='organizationUnits/parentOrgUnitId') - if 'organizationUnits' in orgs and orgs['organizationUnits']: - name = orgs['organizationUnits'][0]['parentOrgUnitId'] - else: - topLevelOrgId = getTopLevelOrgId(cd, '/') - if topLevelOrgId: - name = topLevelOrgId - else: - name = makeOrgUnitPathRelative(name) - result = gapi.call(cd.orgunits(), 'get', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(name)) - if return_attrib: - return result[return_attrib] - display.print_json(result) - if get_users: - name = result['orgUnitPath'] - page_message = gapi.got_total_items_first_last_msg('Users') - users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - message_attribute='primaryEmail', customer=GC_Values[GC_CUSTOMER_ID], query=orgUnitPathQuery(name, checkSuspended), - fields='users(primaryEmail,orgUnitPath),nextPageToken') - if checkSuspended is None: - print('Users:') - elif not checkSuspended: - print('Users (Not suspended):') - else: - print('Users (Suspended):') - for user in users: - if show_children or (name.lower() == user['orgUnitPath'].lower()): - sys.stdout.write(f' {user["primaryEmail"]}') - if name.lower() != user['orgUnitPath'].lower(): - print(' (child)') - else: - print('') - -def doGetASPs(users): - cd = buildGAPIObject('directory') - for user in users: - asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user) - if asps: - print(f'Application-Specific Passwords for {user}') - for asp in asps: - if asp['creationTime'] == '0': - created_date = 'Unknown' - else: - created_date = utils.formatTimestampYMDHMS(asp['creationTime']) - if asp['lastTimeUsed'] == '0': - used_date = 'Never' - else: - used_date = utils.formatTimestampYMDHMS(asp['lastTimeUsed']) - print(f' ID: {asp["codeId"]}\n Name: {asp["name"]}\n Created: {created_date}\n Last Used: {used_date}\n') - else: - print(f' no ASPs for {user}\n') - -def doDelASP(users): - cd = buildGAPIObject('directory') - codeIdList = sys.argv[5].lower() - if codeIdList == 'all': - allCodeIds = True - else: - allCodeIds = False - codeIds = codeIdList.replace(',', ' ').split() - for user in users: - if allCodeIds: - asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user, fields='items/codeId') - codeIds = [asp['codeId'] for asp in asps] - for codeId in codeIds: - gapi.call(cd.asps(), 'delete', userKey=user, codeId=codeId) - print(f'deleted ASP {codeId} for {user}') - -def printBackupCodes(user, codes): - jcount = len(codes) - realcount = 0 - for code in codes: - if 'verificationCode' in code and code['verificationCode']: - realcount += 1 - print(f'Backup verification codes for {user} ({realcount})') - print('') - if jcount > 0: - j = 0 - for code in codes: - j += 1 - print(f'{j}. {code["verificationCode"]}') - print('') - -def doGetBackupCodes(users): - cd = buildGAPIObject('directory') - for user in users: - try: - codes = gapi.get_items(cd.verificationCodes(), 'list', 'items', throw_reasons=[gapi.errors.ErrorReason.INVALID_ARGUMENT, gapi.errors.ErrorReason.INVALID], userKey=user) - except (gapi.errors.GapiInvalidArgumentError, gapi.errors.GapiInvalidError): - codes = [] - printBackupCodes(user, codes) - -def doGenBackupCodes(users): - cd = buildGAPIObject('directory') - for user in users: - gapi.call(cd.verificationCodes(), 'generate', userKey=user) - codes = gapi.get_items(cd.verificationCodes(), 'list', 'items', userKey=user) - printBackupCodes(user, codes) - -def doDelBackupCodes(users): - cd = buildGAPIObject('directory') - for user in users: - try: - gapi.call(cd.verificationCodes(), 'invalidate', soft_errors=True, throw_reasons=[gapi.errors.ErrorReason.INVALID], userKey=user) - except gapi.errors.GapiInvalidError: - print(f'No 2SV backup codes for {user}') - continue - print(f'2SV backup codes for {user} invalidated') - -def commonClientIds(clientId): - if clientId == 'gasmo': - return '1095133494869.apps.googleusercontent.com' - return clientId - -def doDelTokens(users): - cd = buildGAPIObject('directory') - clientId = None - i = 5 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'clientid': - clientId = commonClientIds(sys.argv[i+1]) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam delete token") - if not clientId: - controlflow.system_error_exit(3, 'you must specify a clientid for "gam delete token"') - for user in users: - try: - gapi.call(cd.tokens(), 'get', throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND, gapi.errors.ErrorReason.RESOURCE_NOT_FOUND], userKey=user, clientId=clientId) - except (gapi.errors.GapiNotFoundError, gapi.errors.GapiResourceNotFoundError): - print(f'User {user} did not authorize {clientId}') - continue - gapi.call(cd.tokens(), 'delete', userKey=user, clientId=clientId) - print(f'Deleted token for {user}') - -def printShowTokens(i, entityType, users, csvFormat): - def _showToken(token): - print(f' Client ID: {token["clientId"]}') - for item in token: - if item not in ['clientId', 'scopes']: - print(f' {item}: {token.get(item, "")}') - item = 'scopes' - print(f' {item}:') - for it in token.get(item, []): - print(f' {it}') - - cd = buildGAPIObject('directory') - if csvFormat: - todrive = False - titles = ['user', 'clientId', 'displayText', 'anonymous', 'nativeApp', 'userKey', 'scopes'] - csvRows = [] - clientId = None - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if csvFormat and myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'clientid': - clientId = commonClientIds(sys.argv[i+1]) - i += 2 - elif not entityType: - entityType = myarg - users = getUsersToModify(entity_type=entityType, entity=sys.argv[i+1], silent=False) - i += 2 - else: - controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} tokens") - if not entityType: - users = getUsersToModify(entity_type='all', entity='users', silent=False) - fields = ','.join(['clientId', 'displayText', 'anonymous', 'nativeApp', 'userKey', 'scopes']) - i = 0 - count = len(users) - for user in users: - i += 1 - try: - if csvFormat: - sys.stderr.write(f'Getting Access Tokens for {user}\n') - if clientId: - results = [gapi.call(cd.tokens(), 'get', - throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND, gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.RESOURCE_NOT_FOUND], - userKey=user, clientId=clientId, fields=fields)] - else: - results = gapi.get_items(cd.tokens(), 'list', 'items', - throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND], - userKey=user, fields=f'items({fields})') - jcount = len(results) - if not csvFormat: - print(f'User: {user}, Access Tokens{currentCount(i, count)}') - if jcount == 0: - continue - for token in results: - _showToken(token) - else: - if jcount == 0: - continue - for token in results: - row = {'user': user, 'scopes': ' '.join(token.get('scopes', []))} - for item in token: - if item not in ['scopes']: - row[item] = token.get(item, '') - csvRows.append(row) - except (gapi.errors.GapiNotFoundError, gapi.errors.GapiUserNotFoundError, gapi.errors.GapiResourceNotFoundError): - pass - if csvFormat: - display.write_csv_file(csvRows, titles, 'OAuth Tokens', todrive) - -def doDeprovUser(users): - cd = buildGAPIObject('directory') - for user in users: - print(f'Getting Application Specific Passwords for {user}') - asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user, fields='items/codeId') - jcount = len(asps) - if jcount > 0: - j = 0 - for asp in asps: - j += 1 - print(f' deleting ASP {j} of {jcount}') - gapi.call(cd.asps(), 'delete', userKey=user, codeId=asp['codeId']) - else: - print('No ASPs') - print(f'Invalidating 2SV Backup Codes for {user}') - try: - gapi.call(cd.verificationCodes(), 'invalidate', soft_errors=True, throw_reasons=[gapi.errors.ErrorReason.INVALID], userKey=user) - except gapi.errors.GapiInvalidError: - print('No 2SV Backup Codes') - print(f'Getting tokens for {user}...') - tokens = gapi.get_items(cd.tokens(), 'list', 'items', userKey=user, fields='items/clientId') - jcount = len(tokens) - if jcount > 0: - j = 0 - for token in tokens: - j += 1 - print(f' deleting token {j} of {jcount})') - gapi.call(cd.tokens(), 'delete', userKey=user, clientId=token['clientId']) - else: - print('No Tokens') - print(f'Done deprovisioning {user}') - -def doDeleteUser(): - cd = buildGAPIObject('directory') - user_email = normalizeEmailAddressOrUID(sys.argv[3]) - print(f'Deleting account for {user_email}') - gapi.call(cd.users(), 'delete', userKey=user_email) - -def doUndeleteUser(): - cd = buildGAPIObject('directory') - user = normalizeEmailAddressOrUID(sys.argv[3]) - orgUnit = '/' - i = 4 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg in ['ou', 'org']: - orgUnit = makeOrgUnitPathAbsolute(sys.argv[i+1]) - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam undelete user") - if user.find('@') == -1: - user_uid = user - else: - print(f'Looking up UID for {user}...') - deleted_users = gapi.get_all_pages(cd.users(), 'list', 'users', - customer=GC_Values[GC_CUSTOMER_ID], showDeleted=True) - matching_users = list() - for deleted_user in deleted_users: - if str(deleted_user['primaryEmail']).lower() == user: - matching_users.append(deleted_user) - if not matching_users: - controlflow.system_error_exit(3, 'could not find deleted user with that address.') - elif len(matching_users) > 1: - print(f'ERROR: more than one matching deleted {user} user. Please select the correct one to undelete and specify with "gam undelete user uid:"') - print('') - for matching_user in matching_users: - print(f' uid:{matching_user["id"]} ') - for attr_name in ['creationTime', 'lastLoginTime', 'deletionTime']: - try: - if matching_user[attr_name] == NEVER_TIME: - matching_user[attr_name] = 'Never' - print(f' {attr_name}: {matching_user[attr_name]} ') - except KeyError: - pass - print() - sys.exit(3) - else: - user_uid = matching_users[0]['id'] - print(f'Undeleting account for {user}') - gapi.call(cd.users(), 'undelete', userKey=user_uid, body={'orgUnitPath': orgUnit}) - -def doDeleteGroup(): - cd = buildGAPIObject('directory') - group = normalizeEmailAddressOrUID(sys.argv[3]) - print(f'Deleting group {group}') - gapi.call(cd.groups(), 'delete', groupKey=group) - -def doDeleteAlias(alias_email=None): - cd = buildGAPIObject('directory') - is_user = is_group = False - if alias_email is None: - alias_email = sys.argv[3] - if alias_email.lower() == 'user': - is_user = True - alias_email = sys.argv[4] - elif alias_email.lower() == 'group': - is_group = True - alias_email = sys.argv[4] - alias_email = normalizeEmailAddressOrUID(alias_email, noUid=True, noLower=True) - print(f'Deleting alias {alias_email}') - if is_user or (not is_user and not is_group): - try: - gapi.call(cd.users().aliases(), 'delete', throw_reasons=[gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.NOT_FOUND], userKey=alias_email, alias=alias_email) - return - except (gapi.errors.GapiInvalidError, gapi.errors.GapiBadRequestError): - pass - except gapi.errors.GapiNotFoundError: - controlflow.system_error_exit(4, f'The alias {alias_email} does not exist') - if not is_user or (not is_user and not is_group): - gapi.call(cd.groups().aliases(), 'delete', groupKey=alias_email, alias=alias_email) - -def doDeleteOrg(): - cd = buildGAPIObject('directory') - name = getOrgUnitItem(sys.argv[3]) - print(f'Deleting organization {name}') - gapi.call(cd.orgunits(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(name))) - -def send_email(subject, body, recipient=None, sender=None, user=None, method='send', labels=None, msgHeaders={}, kwargs={}): - api_body = {} - default_sender = default_recipient = False - if not user: - user = _getValueFromOAuth('email') - userId, gmail = buildGmailGAPIObject(user) - if not gmail: - return - resource = gmail.users().messages() - if labels: - api_body['labelIds'] = labelsToLabelIds(gmail, labels) - if not sender: - sender = userId - default_sender = True - if not recipient: - recipient = userId - default_recipient = True - # Force ASCII for RFC compliance - # xmlcharref seems to work to display at least - # some unicode in HTML body and is ignored in - # plain text body. - body = body.encode('ascii', 'xmlcharrefreplace').decode() - msg = message_from_string(body) - for header, value in msgHeaders.items(): - msg.__delitem__(header) # can remove multiple case-insensitive matching headers - msg.add_header(header, value) - if subject: - msg.__delitem__('Subject') - msg['Subject'] = subject - if not default_sender: - msg.__delitem__('From') - if not msg['From']: - msg['From'] = sender - if not default_recipient: - msg.__delitem__('to') - if not msg['To']: - msg['To'] = recipient - api_body['raw'] = base64.urlsafe_b64encode(msg.as_bytes()).decode() - if method == 'draft': - resource = gmail.users().drafts() - method = 'create' - api_body = {'message': api_body} - elif method in ['insert', 'import']: - if method == 'import': - method = 'import_' - gapi.call(resource, method, userId=userId, body=api_body, **kwargs) - -USER_ARGUMENT_TO_PROPERTY_MAP = { - 'address': ['addresses',], - 'addresses': ['addresses',], - 'admin': ['isAdmin', 'isDelegatedAdmin',], - 'agreed2terms': ['agreedToTerms',], - 'agreedtoterms': ['agreedToTerms',], - 'aliases': ['aliases', 'nonEditableAliases',], - 'archived': ['archived',], - 'changepassword': ['changePasswordAtNextLogin',], - 'changepasswordatnextlogin': ['changePasswordAtNextLogin',], - 'creationtime': ['creationTime',], - 'deletiontime': ['deletionTime',], - 'email': ['emails',], - 'emails': ['emails',], - 'externalid': ['externalIds',], - 'externalids': ['externalIds',], - 'familyname': ['name.familyName',], - 'firstname': ['name.givenName',], - 'fullname': ['name.fullName',], - 'gal': ['includeInGlobalAddressList',], - 'gender': ['gender.type', 'gender.customGender', 'gender.addressMeAs',], - 'givenname': ['name.givenName',], - 'id': ['id',], - 'im': ['ims',], - 'ims': ['ims',], - 'includeinglobaladdresslist': ['includeInGlobalAddressList',], - 'ipwhitelisted': ['ipWhitelisted',], - 'isadmin': ['isAdmin', 'isDelegatedAdmin',], - 'isdelegatedadmin': ['isAdmin', 'isDelegatedAdmin',], - 'isenforcedin2sv': ['isEnforcedIn2Sv',], - 'isenrolledin2sv': ['isEnrolledIn2Sv',], - 'is2svenforced': ['isEnforcedIn2Sv',], - 'is2svenrolled': ['isEnrolledIn2Sv',], - 'ismailboxsetup': ['isMailboxSetup',], - 'keyword': ['keywords',], - 'keywords': ['keywords',], - 'language': ['languages',], - 'languages': ['languages',], - 'lastlogintime': ['lastLoginTime',], - 'lastname': ['name.familyName',], - 'location': ['locations',], - 'locations': ['locations',], - 'name': ['name.givenName', 'name.familyName', 'name.fullName',], - 'nicknames': ['aliases', 'nonEditableAliases',], - 'noneditablealiases': ['aliases', 'nonEditableAliases',], - 'note': ['notes',], - 'notes': ['notes',], - 'org': ['orgUnitPath',], - 'organization': ['organizations',], - 'organizations': ['organizations',], - 'orgunitpath': ['orgUnitPath',], - 'otheremail': ['emails',], - 'otheremails': ['emails',], - 'ou': ['orgUnitPath',], - 'phone': ['phones',], - 'phones': ['phones',], - 'photo': ['thumbnailPhotoUrl',], - 'photourl': ['thumbnailPhotoUrl',], - 'posix': ['posixAccounts',], - 'posixaccounts': ['posixAccounts',], - 'primaryemail': ['primaryEmail',], - 'recoveryemail': ['recoveryEmail',], - 'recoveryphone': ['recoveryPhone',], - 'relation': ['relations',], - 'relations': ['relations',], - 'ssh': ['sshPublicKeys',], - 'sshkeys': ['sshPublicKeys',], - 'sshpublickeys': ['sshPublicKeys',], - 'suspended': ['suspended', 'suspensionReason',], - 'thumbnailphotourl': ['thumbnailPhotoUrl',], - 'username': ['primaryEmail',], - 'website': ['websites',], - 'websites': ['websites',], - } - -def doPrintUsers(): - cd = buildGAPIObject('directory') - todrive = False - fieldsList = [] - fieldsTitles = {} - titles = [] - csvRows = [] - display.add_field_to_csv_file('primaryemail', USER_ARGUMENT_TO_PROPERTY_MAP, fieldsList, fieldsTitles, titles) - customer = GC_Values[GC_CUSTOMER_ID] - domain = None - queries = [None] - projection = 'basic' - customFieldMask = None - sortHeaders = getGroupFeed = getLicenseFeed = email_parts = False - viewType = deleted_only = orderBy = sortOrder = None - groupDelimiter = ' ' - licenseDelimiter = ',' - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg in PROJECTION_CHOICES_MAP: - projection = myarg - sortHeaders = True - fieldsList = [] - i += 1 - elif myarg == 'allfields': - projection = 'basic' - sortHeaders = True - fieldsList = [] - i += 1 - elif myarg == 'delimiter': - groupDelimiter = licenseDelimiter = sys.argv[i+1] - i += 2 - elif myarg == 'sortheaders': - sortHeaders = True - i += 1 - elif myarg in ['custom', 'schemas']: - fieldsList.append('customSchemas') - if sys.argv[i+1].lower() == 'all': - projection = 'full' - else: - projection = 'custom' - customFieldMask = sys.argv[i+1] - i += 2 - elif myarg == 'todrive': - todrive = True - i += 1 - elif myarg in ['deletedonly', 'onlydeleted']: - deleted_only = True - i += 1 - elif myarg == 'orderby': - orderBy = sys.argv[i+1] - validOrderBy = ['email', 'familyname', 'givenname', 'firstname', 'lastname'] - if orderBy.lower() not in validOrderBy: - controlflow.expected_argument_exit("orderby", ", ".join(validOrderBy), orderBy) - if orderBy.lower() in ['familyname', 'lastname']: - orderBy = 'familyName' - elif orderBy.lower() in ['givenname', 'firstname']: - orderBy = 'givenName' - i += 2 - elif myarg == 'userview': - viewType = 'domain_public' - i += 1 - elif myarg in SORTORDER_CHOICES_MAP: - sortOrder = SORTORDER_CHOICES_MAP[myarg] - i += 1 - elif myarg == 'domain': - domain = sys.argv[i+1] - customer = None - i += 2 - elif myarg in ['query', 'queries']: - queries = getQueries(myarg, sys.argv[i+1]) - i += 2 - elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP: - if not fieldsList: - fieldsList = ['primaryEmail',] - display.add_field_to_csv_file(myarg, USER_ARGUMENT_TO_PROPERTY_MAP, fieldsList, fieldsTitles, titles) - i += 1 - elif myarg == 'fields': - if not fieldsList: - fieldsList = ['primaryEmail',] - fieldNameList = sys.argv[i+1] - for field in fieldNameList.lower().replace(',', ' ').split(): - if field in USER_ARGUMENT_TO_PROPERTY_MAP: - display.add_field_to_csv_file(field, USER_ARGUMENT_TO_PROPERTY_MAP, fieldsList, fieldsTitles, titles) - else: - controlflow.invalid_argument_exit(field, "gam print users fields") - i += 2 - elif myarg == 'groups': - getGroupFeed = True - i += 1 - elif myarg in ['license', 'licenses', 'licence', 'licences']: - getLicenseFeed = True - i += 1 - elif myarg in ['emailpart', 'emailparts', 'username']: - email_parts = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print users") - if fieldsList: - fields = f'nextPageToken,users({",".join(set(fieldsList)).replace(".", "/")})' - else: - fields = None - for query in queries: - printGettingAllItems('Users', query) - page_message = gapi.got_total_items_first_last_msg('Users') - all_users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - message_attribute='primaryEmail', customer=customer, domain=domain, fields=fields, - showDeleted=deleted_only, orderBy=orderBy, sortOrder=sortOrder, viewType=viewType, - query=query, projection=projection, customFieldMask=customFieldMask) - for user in all_users: - if email_parts and ('primaryEmail' in user): - user_email = user['primaryEmail'] - if user_email.find('@') != -1: - user['primaryEmailLocal'], user['primaryEmailDomain'] = splitEmailAddress(user_email) - display.add_row_titles_to_csv_file(utils.flatten_json(user), csvRows, titles) - if sortHeaders: - display.sort_csv_titles(['primaryEmail',], titles) - if getGroupFeed: - i = 0 - count = len(csvRows) - titles.append('Groups') - for user in csvRows: - i += 1 - user_email = user['primaryEmail'] - sys.stderr.write(f'Getting Group Membership for {user_email}{currentCountNL(i, count)}') - groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', userKey=user_email) - user['Groups'] = groupDelimiter.join([groupname['email'] for groupname in groups]) - if getLicenseFeed: - titles.append('Licenses') - licenses = doPrintLicenses(returnFields='userId,skuId') - if licenses: - for user in csvRows: - u_licenses = licenses.get(user['primaryEmail'].lower()) - if u_licenses: - user['Licenses'] = licenseDelimiter.join([_skuIdToDisplayName(skuId) for skuId in u_licenses]) - display.write_csv_file(csvRows, titles, 'Users', todrive) - -def doPrintShowAlerts(): - _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) - alerts = gapi.get_all_pages(ac.alerts(), 'list', 'alerts') - titles = [] - csv_rows = [] - for alert in alerts: - aj = utils.flatten_json(alert) - for field in aj: - if field not in titles: - titles.append(field) - csv_rows.append(aj) - display.write_csv_file(csv_rows, titles, 'Alerts', False) - -def doPrintShowAlertFeedback(): - _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) - feedback = gapi.get_all_pages(ac.alerts().feedback(), 'list', 'feedback', alertId='-') - for feedbac in feedback: - print(feedbac) - -def doCreateAlertFeedback(): - _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) - valid_types = gapi.get_enum_values_minus_unspecified(ac._rootDesc['schemas']['AlertFeedback']['properties']['type']['enum']) - alertId = sys.argv[3] - body = {'type': sys.argv[4].upper()} - if body['type'] not in valid_types: - controlflow.system_error_exit(2, f'{body["type"]} is not a valid feedback value, expected one of: {", ".join(valid_types)}') - gapi.call(ac.alerts().feedback(), 'create', alertId=alertId, body=body) - -def doDeleteOrUndeleteAlert(action): - _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) - alertId = sys.argv[3] - kwargs = {} - if action == 'undelete': - kwargs['body'] = {} - gapi.call(ac.alerts(), action, alertId=alertId, **kwargs) - -GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP = { - 'admincreated': ['adminCreated', 'Admin_Created'], - 'aliases': ['aliases', 'Aliases', 'nonEditableAliases', 'NonEditableAliases'], - 'description': ['description', 'Description'], - 'directmemberscount': ['directMembersCount', 'DirectMembersCount'], - 'email': ['email', 'Email'], - 'id': ['id', 'ID'], - 'name': ['name', 'Name'], - } - -GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = { - 'allowexternalmembers': 'allowExternalMembers', - 'allowgooglecommunication': 'allowGoogleCommunication', - 'allowwebposting': 'allowWebPosting', - 'archiveonly': 'archiveOnly', - 'customfootertext': 'customFooterText', - 'customreplyto': 'customReplyTo', - 'defaultmessagedenynotificationtext': 'defaultMessageDenyNotificationText', - 'enablecollaborativeinbox': 'enableCollaborativeInbox', - 'favoriterepliesontop': 'favoriteRepliesOnTop', - 'gal': 'includeInGlobalAddressList', - 'includecustomfooter': 'includeCustomFooter', - 'includeinglobaladdresslist': 'includeInGlobalAddressList', - 'isarchived': 'isArchived', - 'memberscanpostasthegroup': 'membersCanPostAsTheGroup', - 'messagemoderationlevel': 'messageModerationLevel', - 'primarylanguage': 'primaryLanguage', - 'replyto': 'replyTo', - 'sendmessagedenynotification': 'sendMessageDenyNotification', - 'showingroupdirectory': 'showInGroupDirectory', - 'spammoderationlevel': 'spamModerationLevel', - 'whocanadd': 'whoCanAdd', - 'whocanapprovemembers': 'whoCanApproveMembers', - 'whocanapprovemessages': 'whoCanApproveMessages', - 'whocanassigntopics': 'whoCanAssignTopics', - 'whocanassistcontent': 'whoCanAssistContent', - 'whocanbanusers': 'whoCanBanUsers', - 'whocancontactowner': 'whoCanContactOwner', - 'whocandeleteanypost': 'whoCanDeleteAnyPost', - 'whocandeletetopics': 'whoCanDeleteTopics', - 'whocandiscovergroup': 'whoCanDiscoverGroup', - 'whocanenterfreeformtags': 'whoCanEnterFreeFormTags', - 'whocanhideabuse': 'whoCanHideAbuse', - 'whocaninvite': 'whoCanInvite', - 'whocanjoin': 'whoCanJoin', - 'whocanleavegroup': 'whoCanLeaveGroup', - 'whocanlocktopics': 'whoCanLockTopics', - 'whocanmaketopicssticky': 'whoCanMakeTopicsSticky', - 'whocanmarkduplicate': 'whoCanMarkDuplicate', - 'whocanmarkfavoritereplyonanytopic': 'whoCanMarkFavoriteReplyOnAnyTopic', - 'whocanmarkfavoritereplyonowntopic': 'whoCanMarkFavoriteReplyOnOwnTopic', - 'whocanmarknoresponseneeded': 'whoCanMarkNoResponseNeeded', - 'whocanmoderatecontent': 'whoCanModerateContent', - 'whocanmoderatemembers': 'whoCanModerateMembers', - 'whocanmodifymembers': 'whoCanModifyMembers', - 'whocanmodifytagsandcategories': 'whoCanModifyTagsAndCategories', - 'whocanmovetopicsin': 'whoCanMoveTopicsIn', - 'whocanmovetopicsout': 'whoCanMoveTopicsOut', - 'whocanpostannouncements': 'whoCanPostAnnouncements', - 'whocanpostmessage': 'whoCanPostMessage', - 'whocantaketopics': 'whoCanTakeTopics', - 'whocanunassigntopic': 'whoCanUnassignTopic', - 'whocanunmarkfavoritereplyonanytopic': 'whoCanUnmarkFavoriteReplyOnAnyTopic', - 'whocanviewgroup': 'whoCanViewGroup', - 'whocanviewmembership': 'whoCanViewMembership', - } - -def doPrintGroups(): - cd = buildGAPIObject('directory') - i = 3 - members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False - customer = GC_Values[GC_CUSTOMER_ID] - usedomain = usemember = usequery = None - aliasDelimiter = ' ' - memberDelimiter = '\n' - todrive = False - cdfieldsList = [] - gsfieldsList = [] - fieldsTitles = {} - titles = [] - csvRows = [] - display.add_field_title_to_csv_file('email', GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) - roles = [] - getSettings = sortHeaders = False - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'domain': - usedomain = sys.argv[i+1].lower() - customer = None - i += 2 - elif myarg == 'member': - usemember = normalizeEmailAddressOrUID(sys.argv[i+1]) - customer = usequery = None - i += 2 - elif myarg == 'query': - usequery = sys.argv[i+1] - usemember = None - i += 2 - elif myarg == 'maxresults': - # deprecated argument - i += 2 - elif myarg == 'delimiter': - aliasDelimiter = memberDelimiter = sys.argv[i+1] - i += 2 - elif myarg in GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP: - display.add_field_title_to_csv_file(myarg, GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) - i += 1 - elif myarg == 'settings': - getSettings = True - i += 1 - elif myarg == 'allfields': - getSettings = sortHeaders = True - cdfieldsList = [] - gsfieldsList = [] - fieldsTitles = {} - for field in GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP: - display.add_field_title_to_csv_file(field, GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) - i += 1 - elif myarg == 'sortheaders': - sortHeaders = True - i += 1 - elif myarg == 'fields': - fieldNameList = sys.argv[i+1] - for field in fieldNameList.lower().replace(',', ' ').split(): - if field in GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP: - display.add_field_title_to_csv_file(field, GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) - elif field in GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP: - display.add_field_to_csv_file(field, {field: [GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP[field]]}, gsfieldsList, fieldsTitles, titles) - elif field == 'collaborative': - for attrName in COLLABORATIVE_INBOX_ATTRIBUTES: - display.add_field_to_csv_file(attrName, {attrName: [attrName]}, gsfieldsList, fieldsTitles, titles) - else: - controlflow.invalid_argument_exit(field, "gam print groups fields") - i += 2 - elif myarg in ['members', 'memberscount']: - roles.append(ROLE_MEMBER) - members = True - if myarg == 'memberscount': - membersCountOnly = True - i += 1 - elif myarg in ['owners', 'ownerscount']: - roles.append(ROLE_OWNER) - owners = True - if myarg == 'ownerscount': - ownersCountOnly = True - i += 1 - elif myarg in ['managers', 'managerscount']: - roles.append(ROLE_MANAGER) - managers = True - if myarg == 'managerscount': - managersCountOnly = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print groups") - cdfields = ','.join(set(cdfieldsList)) - if gsfieldsList: - getSettings = True - gsfields = ','.join(set(gsfieldsList)) - elif getSettings: - gsfields = None - if getSettings: - gs = buildGAPIObject('groupssettings') - roles = ','.join(sorted(set(roles))) - if roles: - if members: - display.add_titles_to_csv_file(['MembersCount',], titles) - if not membersCountOnly: - display.add_titles_to_csv_file(['Members',], titles) - if managers: - display.add_titles_to_csv_file(['ManagersCount',], titles) - if not managersCountOnly: - display.add_titles_to_csv_file(['Managers',], titles) - if owners: - display.add_titles_to_csv_file(['OwnersCount',], titles) - if not ownersCountOnly: - display.add_titles_to_csv_file(['Owners',], titles) - printGettingAllItems('Groups', None) - page_message = gapi.got_total_items_first_last_msg('Groups') - entityList = gapi.get_all_pages(cd.groups(), 'list', 'groups', - page_message=page_message, message_attribute='email', - customer=customer, domain=usedomain, userKey=usemember, query=usequery, - fields=f'nextPageToken,groups({cdfields})') - i = 0 - count = len(entityList) - for groupEntity in entityList: - i += 1 - groupEmail = groupEntity['email'] - group = {} - for field in cdfieldsList: - if field in groupEntity: - if isinstance(groupEntity[field], list): - group[fieldsTitles[field]] = aliasDelimiter.join(groupEntity[field]) - else: - group[fieldsTitles[field]] = groupEntity[field] - if roles: - sys.stderr.write(f' Getting {roles} for {groupEmail}{currentCountNL(i, count)}') - page_message = gapi.got_total_items_first_last_msg('Members') - validRoles, listRoles, listFields = _getRoleVerification(roles, 'nextPageToken,members(email,id,role)') - groupMembers = gapi.get_all_pages(cd.members(), 'list', 'members', - page_message=page_message, message_attribute='email', - soft_errors=True, - groupKey=groupEmail, roles=listRoles, fields=listFields) - if members: - membersList = [] - membersCount = 0 - if managers: - managersList = [] - managersCount = 0 - if owners: - ownersList = [] - ownersCount = 0 - for member in groupMembers: - member_email = member.get('email', member.get('id', None)) - if not member_email: - sys.stderr.write(f' Not sure what to do with: {member}') - continue - role = member.get('role', ROLE_MEMBER) - if not validRoles or role in validRoles: - if role == ROLE_MEMBER: - if members: - membersCount += 1 - if not membersCountOnly: - membersList.append(member_email) - elif role == ROLE_MANAGER: - if managers: - managersCount += 1 - if not managersCountOnly: - managersList.append(member_email) - elif role == ROLE_OWNER: - if owners: - ownersCount += 1 - if not ownersCountOnly: - ownersList.append(member_email) - elif members: - membersCount += 1 - if not membersCountOnly: - membersList.append(member_email) - if members: - group['MembersCount'] = membersCount - if not membersCountOnly: - group['Members'] = memberDelimiter.join(membersList) - if managers: - group['ManagersCount'] = managersCount - if not managersCountOnly: - group['Managers'] = memberDelimiter.join(managersList) - if owners: - group['OwnersCount'] = ownersCount - if not ownersCountOnly: - group['Owners'] = memberDelimiter.join(ownersList) - if getSettings and not GroupIsAbuseOrPostmaster(groupEmail): - sys.stderr.write(f' Retrieving Settings for group {groupEmail}{currentCountNL(i, count)}') - settings = gapi.call(gs.groups(), 'get', - soft_errors=True, - retry_reasons=[gapi.errors.ErrorReason.SERVICE_LIMIT, gapi.errors.ErrorReason.INVALID], - groupUniqueId=groupEmail, fields=gsfields) - if settings: - for key in settings: - if key in ['email', 'name', 'description', 'kind', 'etag']: - continue - setting_value = settings[key] - if setting_value is None: - setting_value = '' - if key not in titles: - titles.append(key) - group[key] = setting_value - else: - sys.stderr.write(f" Settings unavailable for group {groupEmail}{currentCountNL(i, count)}") - csvRows.append(group) - if sortHeaders: - display.sort_csv_titles(['Email',], titles) - display.write_csv_file(csvRows, titles, 'Groups', todrive) - -def doPrintOrgs(): - print_order = ['orgUnitPath', 'orgUnitId', 'name', 'description', - 'parentOrgUnitPath', 'parentOrgUnitId', 'blockInheritance'] - cd = buildGAPIObject('directory') - listType = 'all' - orgUnitPath = "/" - todrive = False - fields = ['orgUnitPath', 'name', 'orgUnitId', 'parentOrgUnitId'] - titles = [] - csvRows = [] - parentOrgIds = [] - retrievedOrgIds = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'toplevelonly': - listType = 'children' - i += 1 - elif myarg == 'fromparent': - orgUnitPath = getOrgUnitItem(sys.argv[i+1]) - i += 2 - elif myarg == 'allfields': - fields = None - i += 1 - elif myarg == 'fields': - fields += sys.argv[i+1].split(',') - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print orgs") - printGettingAllItems('Organizational Units', None) - if fields: - get_fields = ','.join(fields) - list_fields = f'organizationUnits({get_fields})' - else: - list_fields = None - get_fields = None - orgs = gapi.call(cd.orgunits(), 'list', - customerId=GC_Values[GC_CUSTOMER_ID], type=listType, orgUnitPath=orgUnitPath, fields=list_fields) - if not 'organizationUnits' in orgs: - topLevelOrgId = getTopLevelOrgId(cd, orgUnitPath) - if topLevelOrgId: - parentOrgIds.append(topLevelOrgId) - orgunits = [] - else: - orgunits = orgs['organizationUnits'] - for row in orgunits: - retrievedOrgIds.append(row['orgUnitId']) - if row['parentOrgUnitId'] not in parentOrgIds: - parentOrgIds.append(row['parentOrgUnitId']) - missing_parents = set(parentOrgIds) - set(retrievedOrgIds) - for missing_parent in missing_parents: - try: - result = gapi.call(cd.orgunits(), 'get', throw_reasons=['required'], - customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=missing_parent, fields=get_fields) - orgunits.append(result) - except: - pass - for row in orgunits: - orgEntity = {} - for key, value in list(row.items()): - if key in ['kind', 'etag', 'etags']: - continue - if key not in titles: - titles.append(key) - orgEntity[key] = value - csvRows.append(orgEntity) - for title in titles: - if title not in print_order: - print_order.append(title) - titles = sorted(titles, key=print_order.index) - # sort results similar to how they list in admin console - csvRows.sort(key=lambda x: x['orgUnitPath'].lower(), reverse=False) - display.write_csv_file(csvRows, titles, 'Orgs', todrive) - -def doPrintAliases(): - cd = buildGAPIObject('directory') - todrive = False - titles = ['Alias', 'Target', 'TargetType'] - csvRows = [] - userFields = ['primaryEmail', 'aliases'] - groupFields = ['email', 'aliases'] - doGroups = doUsers = True - queries = [None] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'shownoneditable': - titles.insert(1, 'NonEditableAlias') - userFields.append('nonEditableAliases') - groupFields.append('nonEditableAliases') - i += 1 - elif myarg == 'nogroups': - doGroups = False - i += 1 - elif myarg == 'nousers': - doUsers = False - i += 1 - elif myarg in ['query', 'queries']: - queries = getQueries(myarg, sys.argv[i+1]) - doGroups = False - doUsers = True - i += 2 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print aliases") - if doUsers: - for query in queries: - printGettingAllItems('User Aliases', query) - page_message = gapi.got_total_items_first_last_msg('Users') - all_users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - message_attribute='primaryEmail', customer=GC_Values[GC_CUSTOMER_ID], query=query, - fields=f'nextPageToken,users({",".join(userFields)})') - for user in all_users: - for alias in user.get('aliases', []): - csvRows.append({'Alias': alias, 'Target': user['primaryEmail'], 'TargetType': 'User'}) - for alias in user.get('nonEditableAliases', []): - csvRows.append({'NonEditableAlias': alias, 'Target': user['primaryEmail'], 'TargetType': 'User'}) - if doGroups: - printGettingAllItems('Group Aliases', None) - page_message = gapi.got_total_items_first_last_msg('Groups') - all_groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', page_message=page_message, - message_attribute='email', customer=GC_Values[GC_CUSTOMER_ID], - fields=f'nextPageToken,groups({",".join(groupFields)})') - for group in all_groups: - for alias in group.get('aliases', []): - csvRows.append({'Alias': alias, 'Target': group['email'], 'TargetType': 'Group'}) - for alias in group.get('nonEditableAliases', []): - csvRows.append({'NonEditableAlias': alias, 'Target': group['email'], 'TargetType': 'Group'}) - display.write_csv_file(csvRows, titles, 'Aliases', todrive) - -def doPrintGroupMembers(): - cd = buildGAPIObject('directory') - todrive = False - membernames = False - includeDerivedMembership = False - customer = GC_Values[GC_CUSTOMER_ID] - checkSuspended = usedomain = usemember = usequery = None - roles = [] - fields = 'nextPageToken,members(email,id,role,status,type)' - titles = ['group'] - csvRows = [] - groups_to_get = [] - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg == 'domain': - usedomain = sys.argv[i+1].lower() - customer = None - i += 2 - elif myarg == 'member': - usemember = normalizeEmailAddressOrUID(sys.argv[i+1]) - customer = usequery = None - i += 2 - elif myarg == 'query': - usequery = sys.argv[i+1] - usemember = None - i += 2 - elif myarg == 'fields': - memberFieldsList = sys.argv[i+1].replace(',', ' ').lower().split() - fields = f'nextPageToken,members({",".join(memberFieldsList)})' - i += 2 - elif myarg == 'membernames': - membernames = True - titles.append('name') - i += 1 - elif myarg in ['role', 'roles']: - for role in sys.argv[i+1].lower().replace(',', ' ').split(): - if role in GROUP_ROLES_MAP: - roles.append(GROUP_ROLES_MAP[role]) - else: - controlflow.system_error_exit(2, f'{role} is not a valid role for "gam print group-members {myarg}"') - i += 2 - elif myarg in ['group', 'groupns', 'groupsusp']: - group_email = normalizeEmailAddressOrUID(sys.argv[i+1]) - groups_to_get = [{'email': group_email}] - if myarg == 'groupns': - checkSuspended = False - elif myarg == 'groupsusp': - checkSuspended = True - i += 2 - elif myarg in ['suspended', 'notsuspended']: - checkSuspended = myarg == 'suspended' - i += 1 - elif myarg == 'includederivedmembership': - includeDerivedMembership = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print group-members") - if not groups_to_get: - groups_to_get = gapi.get_all_pages(cd.groups(), 'list', 'groups', message_attribute='email', - customer=customer, domain=usedomain, userKey=usemember, query=usequery, - fields='nextPageToken,groups(email)') - i = 0 - count = len(groups_to_get) - for group in groups_to_get: - i += 1 - group_email = group['email'] - sys.stderr.write(f'Getting members for {group_email}{currentCountNL(i, count)}') - validRoles, listRoles, listFields = _getRoleVerification(','.join(roles), fields) - group_members = gapi.get_all_pages(cd.members(), 'list', 'members', - soft_errors=True, - includeDerivedMembership=includeDerivedMembership, - groupKey=group_email, roles=listRoles, fields=listFields) - for member in group_members: - if not _checkMemberRoleIsSuspended(member, validRoles, checkSuspended): - continue - for title in member: - if title not in titles: - titles.append(title) - member['group'] = group_email - if membernames and 'type' in member and 'id' in member: - if member['type'] == 'USER': - try: - mbinfo = gapi.call(cd.users(), 'get', - throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.NOT_FOUND, gapi.errors.ErrorReason.FORBIDDEN], - userKey=member['id'], fields='name') - memberName = mbinfo['name']['fullName'] - except (gapi.errors.GapiUserNotFoundError, gapi.errors.GapiNotFoundError, gapi.errors.GapiForbiddenError): - memberName = 'Unknown' - elif member['type'] == 'GROUP': - try: - mbinfo = gapi.call(cd.groups(), 'get', - throw_reasons=[gapi.errors.ErrorReason.NOT_FOUND, gapi.errors.ErrorReason.FORBIDDEN], - groupKey=member['id'], fields='name') - memberName = mbinfo['name'] - except (gapi.errors.GapiNotFoundError, gapi.errors.GapiForbiddenError): - memberName = 'Unknown' - elif member['type'] == 'CUSTOMER': - try: - mbinfo = gapi.call(cd.customers(), 'get', - throw_reasons=[gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.RESOURCE_NOT_FOUND, gapi.errors.ErrorReason.FORBIDDEN], - customerKey=member['id'], fields='customerDomain') - memberName = mbinfo['customerDomain'] - except (gapi.errors.GapiBadRequestError, gapi.errors.GapiResourceNotFoundError, gapi.errors.GapiForbiddenError): - memberName = 'Unknown' - else: - memberName = 'Unknown' - member['name'] = memberName - csvRows.append(member) - display.write_csv_file(csvRows, titles, 'Group Members', todrive) - -def doPrintMobileDevices(): - cd = buildGAPIObject('directory') - todrive = False - titles = [] - csvRows = [] - fields = None - projection = orderBy = sortOrder = None - queries = [None] - delimiter = ' ' - listLimit = 1 - appsLimit = -1 - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'todrive': - todrive = True - i += 1 - elif myarg in ['query', 'queries']: - queries = getQueries(myarg, sys.argv[i+1]) - i += 2 - elif myarg == 'delimiter': - delimiter = sys.argv[i+1] - i += 2 - elif myarg == 'listlimit': - listLimit = getInteger(sys.argv[i+1], myarg, minVal=-1) - i += 2 - elif myarg == 'appslimit': - appsLimit = getInteger(sys.argv[i+1], myarg, minVal=-1) - i += 2 - elif myarg == 'fields': - fields = f'nextPageToken,mobiledevices({sys.argv[i+1]})' - i += 2 - elif myarg == 'orderby': - orderBy = sys.argv[i+1].lower() - validOrderBy = ['deviceid', 'email', 'lastsync', 'model', 'name', 'os', 'status', 'type'] - if orderBy not in validOrderBy: - controlflow.expected_argument_exit("orderby", ", ".join(validOrderBy), orderBy) - if orderBy == 'lastsync': - orderBy = 'lastSync' - elif orderBy == 'deviceid': - orderBy = 'deviceId' - i += 2 - elif myarg in SORTORDER_CHOICES_MAP: - sortOrder = SORTORDER_CHOICES_MAP[myarg] - i += 1 - elif myarg in PROJECTION_CHOICES_MAP: - projection = PROJECTION_CHOICES_MAP[myarg] - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print mobile") - for query in queries: - printGettingAllItems('Mobile Devices', query) - page_message = gapi.got_total_items_msg('Mobile Devices', '...\n') - all_mobile = gapi.get_all_pages(cd.mobiledevices(), 'list', 'mobiledevices', page_message=page_message, - customerId=GC_Values[GC_CUSTOMER_ID], query=query, projection=projection, fields=fields, - orderBy=orderBy, sortOrder=sortOrder) - for mobile in all_mobile: - row = {} - for attrib in mobile: - if attrib in ['kind', 'etag']: - continue - if attrib in ['name', 'email', 'otherAccountsInfo']: - if attrib not in titles: - titles.append(attrib) - if listLimit > 0: - row[attrib] = delimiter.join(mobile[attrib][0:listLimit]) - elif listLimit == 0: - row[attrib] = delimiter.join(mobile[attrib]) - elif attrib == 'applications': - if appsLimit >= 0: - if attrib not in titles: - titles.append(attrib) - applications = [] - j = 0 - for app in mobile[attrib]: - j += 1 - if appsLimit and (j > appsLimit): - break - appDetails = [] - for field in ['displayName', 'packageName', 'versionName']: - appDetails.append(app.get(field, '')) - appDetails.append(str(app.get('versionCode', ''))) - permissions = app.get('permission', []) - if permissions: - appDetails.append('/'.join(permissions)) - else: - appDetails.append('') - applications.append('-'.join(appDetails)) - row[attrib] = delimiter.join(applications) - else: - if attrib not in titles: - titles.append(attrib) - if attrib == 'deviceId': - row[attrib] = mobile[attrib].encode('unicode-escape').decode(UTF8) - elif attrib == 'securityPatchLevel' and int(mobile[attrib]): - row[attrib] = utils.formatTimestampYMDHMS(mobile[attrib]) - else: - row[attrib] = mobile[attrib] - csvRows.append(row) - display.sort_csv_titles(['resourceId', 'deviceId', 'serialNumber', 'name', 'email', 'status'], titles) - display.write_csv_file(csvRows, titles, 'Mobile', todrive) - -def doPrintLicenses(returnFields=None, skus=None, countsOnly=False, returnCounts=False): - lic = buildGAPIObject('licensing') - products = [] - licenses = [] - licenseCounts = [] - if not returnFields: - csvRows = [] - todrive = False - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower() - if not returnCounts and myarg == 'todrive': - todrive = True - i += 1 - elif myarg in ['products', 'product']: - products = sys.argv[i+1].split(',') - i += 2 - elif myarg in ['sku', 'skus']: - skus = sys.argv[i+1].split(',') - i += 2 - elif myarg == 'allskus': - skus = sorted(SKUS) - products = [] - i += 1 - elif myarg == 'gsuite': - skus = [skuId for skuId in SKUS if SKUS[skuId]['product'] in ['Google-Apps', '101031']] - products = [] - i += 1 - elif myarg == 'countsonly': - countsOnly = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam print licenses") - if not countsOnly: - fields = 'nextPageToken,items(productId,skuId,userId)' - titles = ['userId', 'productId', 'skuId'] - else: - fields = 'nextPageToken,items(userId)' - if not returnCounts: - if skus: - titles = ['productId', 'skuId', 'licenses'] - else: - titles = ['productId', 'licenses'] - else: - fields = f'nextPageToken,items({returnFields})' - if skus: - for sku in skus: - if not products: - product, sku = getProductAndSKU(sku) - else: - product = products[0] - page_message = gapi.got_total_items_msg(f'Licenses for {SKUS.get(sku, {"displayName": sku})["displayName"]}', '...\n') - try: - licenses += gapi.get_all_pages(lic.licenseAssignments(), 'listForProductAndSku', 'items', throw_reasons=[gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.FORBIDDEN], page_message=page_message, - customerId=GC_Values[GC_DOMAIN], productId=product, skuId=sku, fields=fields) - if countsOnly: - licenseCounts.append(['Product', product, 'SKU', sku, 'Licenses', len(licenses)]) - licenses = [] - except (gapi.errors.GapiInvalidError, gapi.errors.GapiForbiddenError): - pass - else: - if not products: - products = sorted(PRODUCTID_NAME_MAPPINGS) - for productId in products: - page_message = gapi.got_total_items_msg(f'Licenses for {PRODUCTID_NAME_MAPPINGS.get(productId, productId)}', '...\n') - try: - licenses += gapi.get_all_pages(lic.licenseAssignments(), 'listForProduct', 'items', throw_reasons=[gapi.errors.ErrorReason.INVALID, gapi.errors.ErrorReason.FORBIDDEN], page_message=page_message, - customerId=GC_Values[GC_DOMAIN], productId=productId, fields=fields) - if countsOnly: - licenseCounts.append(['Product', productId, 'Licenses', len(licenses)]) - licenses = [] - except (gapi.errors.GapiInvalidError, gapi.errors.GapiForbiddenError): - pass - if countsOnly: - if returnCounts: - return licenseCounts - if skus: - for u_license in licenseCounts: - csvRows.append({'productId': u_license[1], 'skuId': u_license[3], 'licenses': u_license[5]}) - else: - for u_license in licenseCounts: - csvRows.append({'productId': u_license[1], 'licenses': u_license[3]}) - display.write_csv_file(csvRows, titles, 'Licenses', todrive) - return - if returnFields: - if returnFields == 'userId': - userIds = [] - for u_license in licenses: - userId = u_license.get('userId', '').lower() - if userId: - userIds.append(userId) - return userIds - userSkuIds = {} - for u_license in licenses: - userId = u_license.get('userId', '').lower() - skuId = u_license.get('skuId') - if userId and skuId: - userSkuIds.setdefault(userId, []) - userSkuIds[userId].append(skuId) - return userSkuIds - for u_license in licenses: - userId = u_license.get('userId', '').lower() - skuId = u_license.get('skuId', '') - csvRows.append({'userId': userId, 'productId': u_license.get('productId', ''), - 'skuId': _skuIdToDisplayName(skuId)}) - display.write_csv_file(csvRows, titles, 'Licenses', todrive) - -def doShowLicenses(): - licenseCounts = doPrintLicenses(countsOnly=True, returnCounts=True) - for u_license in licenseCounts: - line = '' - for i in range(0, len(u_license), 2): - line += f'{u_license[i]}: {u_license[i+1]}, ' - print(line[:-2]) - -def shlexSplitList(entity, dataDelimiter=' ,'): - lexer = shlex.shlex(entity, posix=True) - lexer.whitespace = dataDelimiter - lexer.whitespace_split = True - return list(lexer) - -def shlexSplitListStatus(entity, dataDelimiter=' ,'): - lexer = shlex.shlex(entity, posix=True) - lexer.whitespace = dataDelimiter - lexer.whitespace_split = True - try: - return (True, list(lexer)) - except ValueError as e: - return (False, str(e)) - -def getQueries(myarg, argstr): - if myarg == 'query': - return [argstr] - return shlexSplitList(argstr) - -def _getRoleVerification(memberRoles, fields): - if memberRoles and memberRoles.find(ROLE_MEMBER) != -1: - return (set(memberRoles.split(',')), None, fields if fields.find('role') != -1 else fields[:-1]+',role)') - return (set(), memberRoles, fields) - -def getUsersToModify(entity_type=None, entity=None, silent=False, member_type=None, checkSuspended=None, groupUserMembersOnly=True): - got_uids = False - if entity_type is None: - entity_type = sys.argv[1].lower() - if entity is None: - entity = sys.argv[2] - cd = buildGAPIObject('directory') - if entity_type == 'user': - users = [entity,] - elif entity_type == 'users': - users = entity.replace(',', ' ').split() - elif entity_type in ['group', 'group_ns', 'group_susp']: - if entity_type == 'group_ns': - checkSuspended = False - elif entity_type == 'group_susp': - checkSuspended = True - got_uids = True - group = entity - if member_type is None: - member_type_message = 'all members' - else: - member_type_message = f'{member_type.lower()}s' - group = normalizeEmailAddressOrUID(group) - page_message = None - if not silent: - sys.stderr.write(f'Getting {member_type_message} of {group} (may take some time for large groups)...\n') - page_message = gapi.got_total_items_msg(f'{member_type_message}', '...') - validRoles, listRoles, listFields = _getRoleVerification(member_type, 'nextPageToken,members(email,id,type,status)') - members = gapi.get_all_pages(cd.members(), 'list', 'members', page_message=page_message, - groupKey=group, roles=listRoles, fields=listFields) - users = [] - for member in members: - if ((not groupUserMembersOnly) or (member['type'] == 'USER')) and _checkMemberRoleIsSuspended(member, validRoles, checkSuspended): - users.append(member.get('email', member['id'])) - elif entity_type in ['ou', 'org', 'ou_ns', 'org_ns', 'ou_susp', 'org_susp',]: - if entity_type in ['ou_ns', 'org_ns']: - checkSuspended = False - elif entity_type in ['ou_susp', 'org_susp']: - checkSuspended = True - got_uids = True - ou = makeOrgUnitPathAbsolute(entity) - users = [] - if ou.startswith('id:'): - ou = gapi.call(cd.orgunits(), 'get', - customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=ou, fields='orgUnitPath')['orgUnitPath'] - query = orgUnitPathQuery(ou, checkSuspended) - page_message = None - if not silent: - printGettingAllItems('Users', query) - page_message = gapi.got_total_items_msg('Users', '...') - members = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,users(primaryEmail,orgUnitPath)', - query=query) - ou = ou.lower() - for member in members: - if ou == member.get('orgUnitPath', '').lower(): - users.append(member['primaryEmail']) - if not silent: - sys.stderr.write(f'{len(users)} Users are directly in the OU.\n') - elif entity_type in ['ou_and_children', 'ou_and_child', 'ou_and_children_ns', 'ou_and_child_ns', 'ou_and_children_susp', 'ou_and_child_susp']: - if entity_type in ['ou_and_children_ns', 'ou_and_child_ns']: - checkSuspended = False - elif entity_type in ['ou_and_children_susp', 'ou_and_child_susp']: - checkSuspended = True - got_uids = True - ou = makeOrgUnitPathAbsolute(entity) - users = [] - query = orgUnitPathQuery(ou, checkSuspended) - page_message = None - if not silent: - printGettingAllItems('Users', query) - page_message = gapi.got_total_items_msg('Users', '...') - members = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,users(primaryEmail)', - query=query) - for member in members: - users.append(member['primaryEmail']) - if not silent: - sys.stderr.write("done.\r\n") - elif entity_type in ['query', 'queries']: - if entity_type == 'query': - queries = [entity] - else: - queries = shlexSplitList(entity) - got_uids = True - users = [] - usersSet = set() - for query in queries: - if not silent: - printGettingAllItems('Users', query) - page_message = gapi.got_total_items_msg('Users', '...') - members = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,users(primaryEmail,suspended)', - query=query) - for member in members: - email = member['primaryEmail'] - if (checkSuspended is None or checkSuspended == member['suspended']) and email not in usersSet: - usersSet.add(email) - users.append(email) - if not silent: - sys.stderr.write("done.\r\n") - elif entity_type in ['license', 'licenses', 'licence', 'licences']: - users = doPrintLicenses(returnFields='userId', skus=entity.split(',')) - elif entity_type in ['file', 'crosfile']: - users = [] - f = fileutils.open_file(entity, strip_utf_bom=True) - for row in f: - user = row.strip() - if user: - users.append(user) - fileutils.close_file(f) - if entity_type == 'crosfile': - entity = 'cros' - elif entity_type in ['csv', 'csvfile', 'croscsv', 'croscsvfile']: - drive, filenameColumn = os.path.splitdrive(entity) - if filenameColumn.find(':') == -1: - controlflow.system_error_exit(2, f'Expected {entity_type} FileName:FieldName') - (filename, column) = filenameColumn.split(':') - f = fileutils.open_file(drive+filename) - input_file = csv.DictReader(f, restval='') - if column not in input_file.fieldnames: - controlflow.csv_field_error_exit(column, input_file.fieldnames) - users = [] - for row in input_file: - user = row[column].strip() - if user: - users.append(user) - fileutils.close_file(f) - if entity_type in ['croscsv', 'croscsvfile']: - entity = 'cros' - elif entity_type in ['courseparticipants', 'teachers', 'students']: - croom = buildGAPIObject('classroom') - users = [] - entity = addCourseIdScope(entity) - if entity_type in ['courseparticipants', 'teachers']: - page_message = gapi.got_total_items_msg('Teachers', '...') - teachers = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', page_message=page_message, courseId=entity) - for teacher in teachers: - email = teacher['profile'].get('emailAddress', None) - if email: - users.append(email) - if entity_type in ['courseparticipants', 'students']: - page_message = gapi.got_total_items_msg('Students', '...') - students = gapi.get_all_pages(croom.courses().students(), 'list', 'students', page_message=page_message, courseId=entity) - for student in students: - email = student['profile'].get('emailAddress', None) - if email: - users.append(email) - elif entity_type == 'all': - got_uids = True - users = [] - entity = entity.lower() - if entity == 'users': - query = 'isSuspended=False' - if not silent: - printGettingAllItems('Users', None) - page_message = gapi.got_total_items_msg('Users', '...') - all_users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, - customer=GC_Values[GC_CUSTOMER_ID], query=query, - fields='nextPageToken,users(primaryEmail)') - for member in all_users: - users.append(member['primaryEmail']) - if not silent: - sys.stderr.write(f"done getting {len(users)} Users.\r\n") - elif entity == 'cros': - if not silent: - printGettingAllItems('CrOS Devices', None) - page_message = gapi.got_total_items_msg('CrOS Devices', '...') - all_cros = gapi.get_all_pages(cd.chromeosdevices(), 'list', 'chromeosdevices', page_message=page_message, - customerId=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,chromeosdevices(deviceId)') - for member in all_cros: - users.append(member['deviceId']) - if not silent: - sys.stderr.write(f"done getting {len(users)} CrOS Devices.\r\n") - else: - controlflow.invalid_argument_exit(entity, "gam all") - elif entity_type == 'cros': - users = entity.replace(',', ' ').split() - entity = 'cros' - elif entity_type in ['crosquery', 'crosqueries', 'cros_sn']: - if entity_type == 'cros_sn': - queries = [f'id:{sn}' for sn in shlexSplitList(entity)] - elif entity_type == 'crosqueries': - queries = shlexSplitList(entity) - else: - queries = [entity] - users = [] - usersSet = set() - for query in queries: - if not silent: - printGettingAllItems('CrOS Devices', query) - page_message = gapi.got_total_items_msg('CrOS Devices', '...') - members = gapi.get_all_pages(cd.chromeosdevices(), 'list', 'chromeosdevices', page_message=page_message, - customerId=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,chromeosdevices(deviceId)', - query=query) - for member in members: - deviceId = member['deviceId'] - if deviceId not in usersSet: - usersSet.add(deviceId) - users.append(deviceId) - if not silent: - sys.stderr.write("done.\r\n") - entity = 'cros' - else: - controlflow.invalid_argument_exit(entity_type, "gam") - full_users = list() - if entity != 'cros' and not got_uids: - for user in users: - cg = UID_PATTERN.match(user) - if cg: - full_users.append(cg.group(1)) - elif user != '*' and user != GC_Values[GC_CUSTOMER_ID] and user.find('@') == -1: - full_users.append(f'{user}@{GC_Values[GC_DOMAIN]}') - else: - full_users.append(user) - else: - full_users = users - return full_users - -def OAuthInfo(): - credentials = access_token = id_token = None - show_secret = False - i = 3 - while i < len(sys.argv): - myarg = sys.argv[i].lower().replace('_', '') - if myarg == 'accesstoken': - access_token = sys.argv[i+1] - i += 2 - elif myarg == 'idtoken': - id_token = sys.argv[i+1] - i += 2 - elif myarg == 'showsecret': - show_secret = True - i += 1 - else: - controlflow.invalid_argument_exit(sys.argv[i], "gam oauth info") - if not access_token and not id_token: - credentials = getValidOauth2TxtCredentials() - access_token = credentials.token - print(f'\nOAuth File: {GC_Values[GC_OAUTH2_TXT]}') - oa2 = buildGAPIObject('oauth2') - token_info = gapi.call(oa2, 'tokeninfo', access_token=access_token, id_token=id_token) - if 'issued_to' in token_info: - print(f'Client ID: {token_info["issued_to"]}') - if credentials is not None and show_secret: - print(f'Secret: {credentials.client_secret}') - if 'scope' in token_info: - scopes = token_info['scope'].split(' ') - print(f'Scopes ({len(scopes)})') - for scope in sorted(scopes): - print(f' {scope}') - if 'email' in token_info: - print(f'G Suite Admin: {token_info["email"]}') - if 'expires_in' in token_info: - expires = (datetime.datetime.now() + datetime.timedelta(seconds=token_info['expires_in'])).isoformat() - print(f'Expires: {expires}') - for key, value in token_info.items(): - if key not in ['issued_to', 'scope', 'email', 'expires_in']: - print(f'{key}: {value}') - -def doDeleteOAuth(): - credentials = getOauth2TxtStorageCredentials() - if credentials is None: - return - sys.stderr.write('This OAuth token will self-destruct in 3...') - sys.stderr.flush() - time.sleep(1) - sys.stderr.write('2...') - sys.stderr.flush() - time.sleep(1) - sys.stderr.write('1...') - sys.stderr.flush() - time.sleep(1) - sys.stderr.write('boom!\n') - sys.stderr.flush() - credentials.revoke() - credentials.delete() - -def doRequestOAuth(login_hint=None): - missing_client_secrets_message = ('To use GAM you need to create an API ' - 'project. Please run:\n\ngam create project') - client_secrets_file = GC_Values[GC_CLIENT_SECRETS_JSON] - invalid_client_secrets_format_message = ('The format of your client secrets ' - 'file:\n\n%s\n\nis incorrect. ' - 'Please recreate the file.' % - client_secrets_file) - stored_creds = getOauth2TxtStorageCredentials() - if stored_creds and stored_creds.valid: - print('It looks like you\'ve already authorized GAM. Refusing to overwrite existing file:\n\n%s' % stored_creds.filename) - return - - scopes = getScopesFromUser() - if scopes is None: - # There were no scopes selected. Exit cleanly. - controlflow.system_error_exit(0, '') - login_hint = _getValidateLoginHint(login_hint) - # Needs to be set so oauthlib doesn't puke when Google changes our scopes - os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = 'true' - try: - creds = auth.oauth.Credentials.from_client_secrets_file( - client_secrets_file=client_secrets_file, - scopes=scopes, - access_type='offline', - login_hint=login_hint, - credentials_file=GC_Values[GC_OAUTH2_TXT], - use_console_flow=not GC_Values[GC_OAUTH_BROWSER]) - creds.write() - except auth.oauth.InvalidClientSecretsFileError: - controlflow.system_error_exit(14, missing_client_secrets_message) - except auth.oauth.InvalidClientSecretsFileFormatError: - controlflow.system_error_exit(3, invalid_client_secrets_format_message) - -OAUTH2_SCOPES = [ - {'name': 'Classroom API - counts as 5 scopes', - 'subscopes': [], - 'scopes': ['https://www.googleapis.com/auth/classroom.rosters', - 'https://www.googleapis.com/auth/classroom.courses', - 'https://www.googleapis.com/auth/classroom.profile.emails', - 'https://www.googleapis.com/auth/classroom.profile.photos', - 'https://www.googleapis.com/auth/classroom.guardianlinks.students']}, - {'name': 'Cloud Print API', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/cloudprint'}, - {'name': 'Data Transfer API', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.datatransfer'}, - {'name': 'Directory API - Chrome OS Devices', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromeos'}, - {'name': 'Directory API - Customers', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.customer'}, - {'name': 'Directory API - Domains', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.domain'}, - {'name': 'Directory API - Groups', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.group'}, - {'name': 'Directory API - Mobile Devices', - 'subscopes': ['readonly', 'action'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.device.mobile'}, - {'name': 'Directory API - Organizational Units', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.orgunit'}, - {'name': 'Directory API - Resource Calendars', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.resource.calendar'}, - {'name': 'Directory API - Roles', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.rolemanagement'}, - {'name': 'Directory API - User Schemas', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.userschema'}, - {'name': 'Directory API - User Security', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.user.security'}, - {'name': 'Directory API - Users', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/admin.directory.user'}, - {'name': 'Group Settings API', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/apps.groups.settings'}, - {'name': 'License Manager API', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/apps.licensing'}, - {'name': 'Pub / Sub API', - 'subscopes': [], - 'offByDefault': True, - 'scopes': 'https://www.googleapis.com/auth/pubsub'}, - {'name': 'Reports API - Audit Reports', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/admin.reports.audit.readonly'}, - {'name': 'Reports API - Usage Reports', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/admin.reports.usage.readonly'}, - {'name': 'Reseller API', - 'subscopes': [], - 'offByDefault': True, - 'scopes': 'https://www.googleapis.com/auth/apps.order'}, - {'name': 'Site Verification API', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/siteverification'}, - {'name': 'Vault Matters and Holds API', - 'subscopes': ['readonly'], - 'scopes': 'https://www.googleapis.com/auth/ediscovery'}, - {'name': 'Cloud Storage (Vault Export - read only)', - 'subscopes': [], - 'scopes': 'https://www.googleapis.com/auth/devstorage.read_only'}, - {'name': 'User Profile (Email address - read only)', - 'subscopes': [], - 'scopes': 'email', - 'required': True}, - ] - -def getScopesFromUser(menu_options=None): - """Prompts the user to choose from a list of scopes to authorize. - - Args: - menu_options: An optional list of ScopeMenuOptions to be presented in the - menu. If no menu_options are provided, menu options will be generated - from static OAUTH2_SCOPES definitions. - - Returns: - A list of user-selected scopes to authorize. - """ - - if not menu_options: - menu_options = [ScopeMenuOption.from_gam_oauth2scope(definition) - for definition in OAUTH2_SCOPES] - menu = ScopeSelectionMenu(menu_options) - try: - menu.run() - except ScopeSelectionMenu.UserRequestedExitException: - controlflow.system_error_exit(0, '') - - return menu.get_selected_scopes() - -class ScopeMenuOption(): - """A single GAM API/feature with scopes that can be turned on/off.""" - - def __init__(self, oauth_scopes, description, - is_required=False, - is_selected=False, - supported_restrictions=None, - restriction=None): - """A data structure for storing and toggling feature/API scope attributes. - - Args: - oauth_scopes: A list of Google OAuth scope strings required for the - feature or API. If the applicable scopes can vary in permission level, - the scopes provided in this list should contain the highest level of - permissions. More restrictive scopes are implemented by utilizing the - `supported_restrictions` argument. - description: String, a name or brief description of this API/feature. - is_required: Bool, whether this API/feature is required for GAM. If True, - the ScopeMenuOption cannot be unselected/disabled. - is_selected: Bool, whether the ScopeMenuOption is currently - selected/enabled. - supported_restrictions: A list of strings that can be appended to the - oauth_scopes, separated by '.', to restrict their permissions. - For example, the directory API supports a 'readonly' mode on most - scopes such that - https://www.googleapis.com/auth/admin.directory.domain - becomes - https://www.googleapis.com/auth/admin.directory.domain.readonly - restriction: String, the currently enabled restriction on all scopes in - this ScopeMenuOption. Default is no restrictions (highest permission). - """ - # Initialize private members - self._is_selected = False - self._restriction = None - - self.scopes = oauth_scopes - self.description = description - self.is_required = is_required - # Required scopes must be selected - self.is_selected = is_required or is_selected - self.supported_restrictions = ( - supported_restrictions if supported_restrictions is not None else []) - if restriction: - self.restrict_to(restriction) - - def select(self, restriction=None): - """Selects/enables the ScopeMenuOption with an optional restriction.""" - if restriction is not None: - self.restrict_to(restriction) - self.is_selected = True - - def unselect(self): - """Unselects/disables the ScopeMenuOption.""" - self.is_selected = False - - @property - def is_selected(self): - return self._is_selected - - @is_selected.setter - def is_selected(self, is_selected): - if self.is_required and not is_selected: - raise ValueError('Required scope cannot be unselected') - if not is_selected: - # Disable all applied restrictions - self.unrestrict() - self._is_selected = is_selected - - @property - def is_restricted(self): - return self._restriction is not None - - def supports_restriction(self, restriction): - """Determines if a scope restriction is supported by this ScopeMenuOption. - - Args: - restriction: String, the text appended to a full permission scope which - will restrict its permissiveness. e.g. 'readonly' or 'action'. - - Returns: - Bool, True if the scope restriction can be applied to this option. - """ - return restriction in self.supported_restrictions - - @property - def restriction(self): - return self._restriction - - @restriction.setter - def restriction(self, restriction): - self.restrict_to(restriction) - - def restrict_to(self, restriction): - """Applies a scope restriction to all scopes associated with this option. - - Args: - restriction: String, a scope restriction which is appended to the - full-permission scope with a leading '.'. e.g. if the full scope is - https://www.googleapis.com/auth/admin.directory.domain - providing 'readonly' here will make the effective scope - https://www.googleapis.com/auth/admin.directory.domain.readonly - """ - if self.supports_restriction(restriction): - self._restriction = restriction - else: - error = f'Scope does not support a {restriction} restriction.' - if self.supported_restrictions is not None: - restriction_list = ', '.join(self.supported_restrictions) - error = error + (f' Supported restrictions are: {restriction_list}') - raise ValueError(error) - - def unrestrict(self): - """Removes all scope restrictions currently applied.""" - self._restriction = None - - def get_effective_scopes(self): - """Gets all scopes for this option, including their restrictions. - - Restrictions are applied in the form of trailing text which limit the - scope's capabilities. - """ - effective_scopes = [] - for scope in self.scopes: - if self.is_restricted: - scope = f'{scope}.{self._restriction}' - effective_scopes.append(scope) - return effective_scopes - - @classmethod - def from_gam_oauth2scope(cls, scope_definition): - """Generates a ScopeMenuOption from a dict-style OAUTH2_SCOPES definition. - - Dict fields: - name: Some description of the API/feature. - subscopes: A list of compatible scope restrictions such as 'action' or - 'readonly'. Each scope in the scopes list must support this - restriction text appended to the end of its normal scope text. - scopes: A list of scopes that are required for the API/feature. - offByDefault: A bool indicating whether this feature/scope should be off - by default (when no prior selection has been made). Default is False - (the item will be on/selected by default). - required: A bool indicating the API/feature is required. This scope - cannot be unselected. Default is False. - - Example: - { - 'name': 'Made up API', - 'subscopes': ['action'], - 'scopes': ['https://www.googleapis.com/auth/some.scope'], - } - - Args: - scope_definition: A dict following the syntax of scopes defined in - OAUTH2_SCOPES. - - Returns: - A ScopeMenuOption object. - """ - scopes = scope_definition.get('scopes', []) - # If the scope is a single string, make it into a list. - scope_list = scopes if isinstance(scopes, list) else [scopes] - return cls( - oauth_scopes=scope_list, - description=scope_definition.get('name'), - is_selected=not scope_definition.get('offByDefault'), - supported_restrictions=scope_definition.get('subscopes', []), - is_required=scope_definition.get('required', False)) - -class ScopeSelectionMenu(): - """A text menu which prompts the user to select the scopes to authorize.""" - - class MenuChoiceError(Exception): - """Error when an invalid or incompatible user choice is made.""" - pass - - class UserRequestedExitException(Exception): - """Exception when the user requests immediate exit.""" - pass - - def __init__(self, options): - """A menu of scope options to choose from. - - Args: - options: A list of ScopeMenuOption objects from which to generate the menu - Options will be presented on screen in the same order as the provided - list. - """ - self._options = options - - def get_options(self): - """Returns all options that are available on this menu.""" - return self._options - - def get_selected_options(self): - """Returns all currently selected ScopeMenuOptions.""" - return [option for option in self._options if option.is_selected] - - def get_selected_scopes(self): - """Returns the aggregate set of oauth scopes currently selected.""" - selected_scopes = [scope for option in self.get_selected_options() - for scope in option.get_effective_scopes()] - return list(set(selected_scopes)) - - MENU_CHOICE = { - 'SELECT_ALL_SCOPES': 's', - 'UNSELECT_ALL_SCOPES': 'u', - 'EXIT': 'e', - 'CONTINUE': 'c' - } - - _MENU_DISPLAY_TEXT = ''' -Select the authorized scopes by entering a number. -Append an 'r' to grant read-only access or an 'a' to grant action-only access. - -%s - - s) Select all scopes - u) Unselect all scopes - e) Exit without changes - c) Continue to authorization - -''' - - def get_menu_text(self): - """Returns a text menu with numbered options.""" - scope_menu_items = [ - self._build_scope_menu_item(option, counter) - for counter, option in enumerate(self._options) - ] - return ScopeSelectionMenu._MENU_DISPLAY_TEXT % '\n'.join(scope_menu_items) - - @staticmethod - def _build_scope_menu_item(scope_option, option_number): - """Builds a text line representing a single scope selection in the menu. - - The returned line is in the format: - - [<*>] <##>) () [required] - - * = A single character indicating the option's selection status. - '*' - All scopes are selected with full permissions. - ' ' - No scopes are selected. - 'X' - Where 'X' is the first letter of the selected scope restriction, - such as 'R' for readonly or 'A' for action, indicates scopes are - selected with the corresponding restriction. - ## = The line item number associated to this option. - Feature description = The ScopeMenuOption description. - scope modifiers = The supported scope restrictions for the ScopeMenuOption. - When appended to the unrestricted, full oauth scope, these strings - modify or restrict the access level of the given scope. - [required] = Will only appear if the ScopeMenuOption is required and cannot - be unselected. - - e.g. [*] 2) Directory API - Domains (supports 'readonly') - - Args: - scope_option: The ScopeMenuOption associated with this line item. - option_number: The selectable option number that is associated with - modifying this line item. - - Returns: - A string containing the line item text without a trailing newline. - """ - SELECTION_INDICATOR = { - 'ALL_SELECTED': '*', - 'UNSELECTED': ' ', - } - indicator = SELECTION_INDICATOR['UNSELECTED'] - if scope_option.is_selected: - if scope_option.is_restricted: - # Use the first letter of the restriction as the indicator. - indicator = scope_option.restriction[0].upper() - else: - indicator = SELECTION_INDICATOR['ALL_SELECTED'] - - item_description = [f'[{indicator}]', f'{option_number:2d})', scope_option.description,] - - if scope_option.supported_restrictions: - restrictions = ' and '.join(scope_option.supported_restrictions) - item_description.append(f'(supports {restrictions})') - - if scope_option.is_required: - item_description.append('[required]') - - return ' '.join(item_description) - - def get_prompt_text(self): - """Builds and returns a prompt requesting user input.""" - # Get all the available restrictions and create the list of available - # commands that the user can input. - restrictions = { - restriction for option in self._options - for restriction in option.supported_restrictions - } - restriction_choices = [ - restriction[0].lower() for restriction in restrictions - ] - return ('Please enter 0-%d[%s] or %s: ' % - (len(self._options)-1, # Keep the menu options 0-based - '|'.join(restriction_choices), - '|'.join(list(ScopeSelectionMenu.MENU_CHOICE.values())))) - - def run(self): - """Displays the ScopeSelectionMenu to the user and prompts for input. - - The menu will continue to display until the user finishes adjusting all - desired items and requests to return. - - After the menu is run, callers may use `get_selected_scopes()` or - `get_selected_options()` methods to understand what the user's final choice - was. - - Raises: ScopeSelectionMenu.UserRequestedExitException if the user chooses - to exit the application entirely, rather than continue execution. This - allows callers to decide how to handle the exit. - """ - error_message = None - while True: - os.system(['clear', 'cls'][GM_Globals[GM_WINDOWS]]) - sys.stdout.write(self.get_menu_text()) - if error_message is not None: - colored_error = createRedText(ERROR_PREFIX + error_message + '\n') - sys.stdout.write(colored_error) - error_message = None # Clear the pending error message - - user_input = input(self.get_prompt_text()) - try: - prompt_again = self._process_menu_input(user_input) - if not prompt_again: - return - except ScopeSelectionMenu.MenuChoiceError as e: - error_message = str(e) - - _SINGLE_SCOPE_CHANGE_REGEX = re.compile( - r'\s*(?P\d{1,2})\s*(?P[a-z]?)', re.IGNORECASE) - - # Google-defined maximum number of scopes that can be authorized on a single - # access token. - MAXIMUM_NUM_SCOPES = 50 - - def _process_menu_input(self, raw_menu_input): - """Processes the raw user input provided to the menu prompt. - - Args: - raw_menu_input: The raw, unaltered string provided by the user in response - to the menu prompt. - - Returns: - True, if the user should be prompted for further input. False, if the - user has finished input and requested to continue execution of the - program. - - Raises: - ScopeSelectionMenu.UserRequestedExitException if the user requests to exit - the application immediately. - ScopeSelectionMenu.MenuChoiceError upon invalid user input. - """ - user_input = raw_menu_input.lower().strip() - single_scope_change = ( - ScopeSelectionMenu._SINGLE_SCOPE_CHANGE_REGEX.match(user_input)) - - if single_scope_change: - scope_number, restriction_command = single_scope_change.group( - 'scope_number', 'restriction') - # Make sure we get an actual number to deal with. - scope_number = int(scope_number) - # Scope option numbers displayed in the menu are 0-based and map directly - # to the indices in the list of scopes. - if scope_number < 0 or scope_number > len(self._options) - 1: - raise ScopeSelectionMenu.MenuChoiceError( - f'Invalid scope number "{scope_number}"') - selected_option = self._options[scope_number] - - # Find the restriction that the user intended to apply. - if restriction_command != '': - matching_restrictions = [r for r in selected_option.supported_restrictions if r.startswith(restriction_command)] - if not matching_restrictions: - raise ScopeSelectionMenu.MenuChoiceError( - f'Scope "{selected_option.description}" does not support "{restriction_command}" mode!') - restriction = matching_restrictions[0] - else: - restriction = None - self._update_option(selected_option, restriction=restriction) - - elif user_input == ScopeSelectionMenu.MENU_CHOICE['SELECT_ALL_SCOPES']: - for option in self._options: - self._update_option(option, selected=True) - elif user_input == ScopeSelectionMenu.MENU_CHOICE['UNSELECT_ALL_SCOPES']: - for option in self._options: - # Force-select required options - self._update_option(option, selected=option.is_required) - elif user_input == ScopeSelectionMenu.MENU_CHOICE['CONTINUE']: - return False - elif user_input == ScopeSelectionMenu.MENU_CHOICE['EXIT']: - raise ScopeSelectionMenu.UserRequestedExitException() - else: - raise ScopeSelectionMenu.MenuChoiceError( - f'Invalid input "{user_input}"') - - return True - - def _update_option(self, option, selected=None, restriction=None): - """Validates changes and updates the internal state of options on the menu. - - Args: - option: The ScopeMenuOption to update - selected: If provided, updates the "selected" status of the option. If - not provided, the "selected" status will be toggled to its opposite - state. - restriction: If provided, applies a restriction to the provided option. - - Raises: - ScopeSelectionMenu.MenuChoiceError on change validation errors. - """ - if option.is_required and (not selected or selected is None): - raise ScopeSelectionMenu.MenuChoiceError( - f'Scope "{option.description}" is required and cannot be unselected!') - if selected and not option.is_selected: - # Make sure we're not about to exceed the maximum number of scopes - # authorized on a single token. - num_scopes_to_add = len(option.get_effective_scopes()) - num_selected_scopes = len(self.get_selected_scopes()) - expected_num_scopes = num_scopes_to_add + num_selected_scopes - if expected_num_scopes > ScopeSelectionMenu.MAXIMUM_NUM_SCOPES: - raise ScopeSelectionMenu.MenuChoiceError( - f'Too many scopes selected ({expected_num_scopes}). Maximum is ' - f'{ScopeSelectionMenu.MAXIMUM_NUM_SCOPES}.Please remove some scopes ' - 'and try again.') - - if restriction is None: - if selected is None: - # Toggle the option on/off - option.is_selected = not option.is_selected - else: - option.is_selected = selected - else: - if option.supports_restriction(restriction): - option.select(restriction) - else: - raise ScopeSelectionMenu.MenuChoiceError( - f'Scope "{option.description}" does not support {restriction} mode!') - -def init_gam_worker(): - signal.signal(signal.SIGINT, signal.SIG_IGN) - -def run_batch(items): - if not items: - return - num_worker_threads = min(len(items), GC_Values[GC_NUM_THREADS]) - pool = mp_pool(num_worker_threads, init_gam_worker) - sys.stderr.write(f'Using {num_worker_threads} processes...\n') - try: - results = [] - for item in items: - if item[0] == 'commit-batch': - sys.stderr.write('commit-batch - waiting for running processes to finish before proceeding\n') - pool.close() - pool.join() - pool = mp_pool(num_worker_threads, init_gam_worker) - sys.stderr.write('commit-batch - running processes finished, proceeding\n') - continue - results.append(pool.apply_async(ProcessGAMCommandMulti, [item])) - pool.close() - num_total = len(results) - i = 1 - while True: - num_done = 0 - for r in results: - if r.ready(): - num_done += 1 - if num_done == num_total: - break - i += 1 - if i == 20: - print(f'Finished {num_done} of {num_total} processes.') - i = 1 - time.sleep(1) - except KeyboardInterrupt: - pool.terminate() - pool.join() - -# -# Process command line arguments, find substitutions -# An argument containing instances of ~~xxx~~ has xxx replaced by the value of field xxx from the CSV file -# An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file -# Otherwise, the argument is preserved as is -# -# SubFields is a dictionary; the key is the argument number, the value is a list of tuples that mark -# the substition (fieldname, start, end). -# Example: update user '~User' address type work unstructured '~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~' primary -# {2: [('User', 0, 5)], 7: [('Street', 0, 10), ('City', 12, 20), ('State', 22, 31), ('ZIP', 32, 39)]} -# -def getSubFields(i, fieldNames): - subFields = {} - PATTERN = re.compile(r'~~(.+?)~~') - GAM_argv = [] - GAM_argvI = 0 - while i < len(sys.argv): - myarg = sys.argv[i] - if not myarg: - GAM_argv.append(myarg) - elif PATTERN.search(myarg): - pos = 0 - while True: - match = PATTERN.search(myarg, pos) - if not match: - break - fieldName = match.group(1) - if fieldName in fieldNames: - subFields.setdefault(GAM_argvI, []) - subFields[GAM_argvI].append((fieldName, match.start(), match.end())) - else: - controlflow.csv_field_error_exit(fieldName, fieldNames) - pos = match.end() - GAM_argv.append(myarg) - elif myarg[0] == '~': - fieldName = myarg[1:] - if fieldName in fieldNames: - subFields[GAM_argvI] = [(fieldName, 0, len(myarg))] - GAM_argv.append(myarg) - else: - controlflow.csv_field_error_exit(fieldName, fieldNames) - else: - GAM_argv.append(myarg) - GAM_argvI += 1 - i += 1 - return(GAM_argv, subFields) -# -def processSubFields(GAM_argv, row, subFields): - argv = GAM_argv[:] - for GAM_argvI, fields in subFields.items(): - oargv = argv[GAM_argvI][:] - argv[GAM_argvI] = '' - pos = 0 - for field in fields: - argv[GAM_argvI] += oargv[pos:field[1]] - if row[field[0]]: - argv[GAM_argvI] += row[field[0]] - pos = field[2] - argv[GAM_argvI] += oargv[pos:] - return argv - -def runCmdForUsers(cmd, users, default_to_batch=False, **kwargs): - if default_to_batch and len(users) > 1: - items = [] - for user in users: - items.append(['gam', 'user', user] + sys.argv[3:]) - run_batch(items) - sys.exit(0) - else: - cmd(users, **kwargs) - -def ProcessGAMCommandMulti(args): - ProcessGAMCommand(args) - -# Process GAM command -def ProcessGAMCommand(args): - if args != sys.argv: - sys.argv = args[:] - GM_Globals[GM_SYSEXITRC] = 0 - try: - SetGlobalVariables() - if sys.version_info[1] >= 7: - sys.stdout.reconfigure(encoding=GC_Values[GC_CHARSET], errors='backslashreplace') - sys.stdin.reconfigure(encoding=GC_Values[GC_CHARSET], errors='backslashreplace') - command = sys.argv[1].lower() - if command == 'batch': - i = 2 - filename = sys.argv[i] - i, encoding = getCharSet(i+1) - f = fileutils.open_file(filename, encoding=encoding, strip_utf_bom=True) - items = [] - errors = 0 - for line in f: - try: - argv = shlex.split(line) - except ValueError as e: - sys.stderr.write(f'Command: >>>{line.strip()}<<<\n') - sys.stderr.write(f'{ERROR_PREFIX}{str(e)}\n') - errors += 1 - continue - if argv: - cmd = argv[0].strip().lower() - if (not cmd) or cmd.startswith('#') or ((len(argv) == 1) and (cmd != 'commit-batch')): - continue - if cmd == 'gam': - items.append(argv) - elif cmd == 'commit-batch': - items.append([cmd]) - else: - sys.stderr.write(f'Command: >>>{line.strip()}<<<\n') - sys.stderr.write(f'{ERROR_PREFIX}Invalid: Expected \n') - errors += 1 - fileutils.close_file(f) - if errors == 0: - run_batch(items) - sys.exit(0) - else: - controlflow.system_error_exit(2, f'batch file: {filename}, not processed, {errors} error{["", "s"][errors != 1]}') - elif command == 'csv': - if httplib2.debuglevel > 0: - controlflow.system_error_exit(1, 'CSV commands are not compatible with debug. Delete debug.gam and try again.') - i = 2 - filename = sys.argv[i] - i, encoding = getCharSet(i+1) - f = fileutils.open_file(filename, encoding=encoding) - csvFile = csv.DictReader(f) - if (i == len(sys.argv)) or (sys.argv[i].lower() != 'gam') or (i+1 == len(sys.argv)): - controlflow.system_error_exit(3, '"gam csv " must be followed by a full GAM command...') - i += 1 - GAM_argv, subFields = getSubFields(i, csvFile.fieldnames) - items = [] - for row in csvFile: - items.append(['gam']+processSubFields(GAM_argv, row, subFields)) - fileutils.close_file(f) - run_batch(items) - sys.exit(0) - elif command == 'version': - doGAMVersion() - sys.exit(0) - elif command == 'create': - argument = sys.argv[2].lower() - if argument == 'user': - doCreateUser() - elif argument == 'group': - doCreateGroup() - elif argument in ['nickname', 'alias']: - doCreateAlias() - elif argument in ['org', 'ou']: - doCreateOrg() - elif argument == 'resource': - gapi.directory.resource.createResourceCalendar() - elif argument in ['verify', 'verification']: - doSiteVerifyShow() - elif argument == 'schema': - doCreateOrUpdateUserSchema(False) - elif argument in ['course', 'class']: - doCreateCourse() - elif argument in ['transfer', 'datatransfer']: - doCreateDataTransfer() - elif argument == 'domain': - doCreateDomain() - elif argument in ['domainalias', 'aliasdomain']: - doCreateDomainAlias() - elif argument == 'admin': - doCreateAdmin() - elif argument in ['guardianinvite', 'inviteguardian', 'guardian']: - doInviteGuardian() - elif argument in ['project', 'apiproject']: - doCreateProject() - elif argument in ['resoldcustomer', 'resellercustomer']: - doCreateResoldCustomer() - elif argument in ['resoldsubscription', 'resellersubscription']: - doCreateResoldSubscription() - elif argument in ['matter', 'vaultmatter']: - gapi.vault.createMatter() - elif argument in ['hold', 'vaulthold']: - gapi.vault.createHold() - elif argument in ['export', 'vaultexport']: - gapi.vault.createExport() - elif argument in ['building']: - gapi.directory.resource.createBuilding() - elif argument in ['feature']: - gapi.directory.resource.createFeature() - elif argument in ['alertfeedback']: - doCreateAlertFeedback() - elif argument in ['gcpfolder']: - createGCPFolder() - else: - controlflow.invalid_argument_exit(argument, "gam create") - sys.exit(0) - elif command == 'use': - argument = sys.argv[2].lower() - if argument in ['project', 'apiproject']: - doUseProject() - else: - controlflow.invalid_argument_exit(argument, "gam use") - sys.exit(0) - elif command == 'update': - argument = sys.argv[2].lower() - if argument == 'user': - doUpdateUser(None, 4) - elif argument == 'group': - doUpdateGroup() - elif argument in ['nickname', 'alias']: - doUpdateAlias() - elif argument in ['ou', 'org']: - doUpdateOrg() - elif argument == 'resource': - gapi.directory.resource.updateResourceCalendar() - elif argument == 'cros': - gapi.directory.cros.doUpdateCros() - elif argument == 'mobile': - doUpdateMobile() - elif argument in ['verify', 'verification']: - doSiteVerifyAttempt() - elif argument in ['schema', 'schemas']: - doCreateOrUpdateUserSchema(True) - elif argument in ['course', 'class']: - doUpdateCourse() - elif argument in ['printer', 'print']: - doUpdatePrinter() - elif argument == 'domain': - doUpdateDomain() - elif argument == 'customer': - gapi.directory.customer.doUpdateCustomer() - elif argument in ['resoldcustomer', 'resellercustomer']: - doUpdateResoldCustomer() - elif argument in ['resoldsubscription', 'resellersubscription']: - doUpdateResoldSubscription() - elif argument in ['matter', 'vaultmatter']: - gapi.vault.updateMatter() - elif argument in ['hold', 'vaulthold']: - gapi.vault.updateHold() - elif argument in ['project', 'projects', 'apiproject']: - doUpdateProjects() - elif argument in ['building']: - gapi.directory.resource.updateBuilding() - elif argument in ['feature']: - gapi.directory.resource.updateFeature() - else: - controlflow.invalid_argument_exit(argument, "gam update") - sys.exit(0) - elif command == 'info': - argument = sys.argv[2].lower() - if argument == 'user': - doGetUserInfo() - elif argument == 'group': - doGetGroupInfo() - elif argument == 'member': - doGetMemberInfo() - elif argument in ['nickname', 'alias']: - doGetAliasInfo() - elif argument == 'instance': - gapi.directory.customer.doGetCustomerInfo() - elif argument in ['org', 'ou']: - doGetOrgInfo() - elif argument == 'resource': - gapi.directory.resource.getResourceCalendarInfo() - elif argument == 'cros': - gapi.directory.cros.doGetCrosInfo() - elif argument == 'mobile': - doGetMobileInfo() - elif argument in ['verify', 'verification']: - doGetSiteVerifications() - elif argument in ['schema', 'schemas']: - doGetUserSchema() - elif argument in ['course', 'class']: - doGetCourseInfo() - elif argument in ['printer', 'print']: - doGetPrinterInfo() - elif argument in ['transfer', 'datatransfer']: - doGetDataTransferInfo() - elif argument == 'customer': - gapi.directory.customer.doGetCustomerInfo() - elif argument == 'domain': - doGetDomainInfo() - elif argument in ['domainalias', 'aliasdomain']: - doGetDomainAliasInfo() - elif argument in ['resoldcustomer', 'resellercustomer']: - doGetResoldCustomer() - elif argument in ['resoldsubscription', 'resoldsubscriptions', 'resellersubscription', 'resellersubscriptions']: - doGetResoldSubscriptions() - elif argument in ['matter', 'vaultmatter']: - gapi.vault.getMatterInfo() - elif argument in ['hold', 'vaulthold']: - gapi.vault.getHoldInfo() - elif argument in ['export', 'vaultexport']: - gapi.vault.getExportInfo() - elif argument in ['building']: - gapi.directory.resource.getBuildingInfo() - else: - controlflow.invalid_argument_exit(argument, "gam info") - sys.exit(0) - elif command == 'cancel': - argument = sys.argv[2].lower() - if argument in ['guardianinvitation', 'guardianinvitations']: - doCancelGuardianInvitation() - else: - controlflow.invalid_argument_exit(argument, "gam cancel") - sys.exit(0) - elif command == 'delete': - argument = sys.argv[2].lower() - if argument == 'user': - doDeleteUser() - elif argument == 'group': - doDeleteGroup() - elif argument in ['nickname', 'alias']: - doDeleteAlias() - elif argument == 'org': - doDeleteOrg() - elif argument == 'resource': - gapi.directory.resource.deleteResourceCalendar() - elif argument == 'mobile': - doDeleteMobile() - elif argument in ['schema', 'schemas']: - doDelSchema() - elif argument in ['course', 'class']: - doDelCourse() - elif argument in ['printer', 'printers']: - doDelPrinter() - elif argument == 'domain': - doDelDomain() - elif argument in ['domainalias', 'aliasdomain']: - doDelDomainAlias() - elif argument == 'admin': - doDelAdmin() - elif argument in ['guardian', 'guardians']: - doDeleteGuardian() - elif argument in ['project', 'projects']: - doDelProjects() - elif argument in ['resoldsubscription', 'resellersubscription']: - doDeleteResoldSubscription() - elif argument in ['matter', 'vaultmatter']: - gapi.vault.updateMatter(action=command) - elif argument in ['hold', 'vaulthold']: - gapi.vault.deleteHold() - elif argument in ['export', 'vaultexport']: - gapi.vault.deleteExport() - elif argument in ['building']: - gapi.directory.resource.deleteBuilding() - elif argument in ['feature']: - gapi.directory.resource.deleteFeature() - elif argument in ['alert']: - doDeleteOrUndeleteAlert('delete') - elif argument in ['sakey', 'sakeys']: - doDeleteServiceAccountKeys() - else: - controlflow.invalid_argument_exit(argument, "gam delete") - sys.exit(0) - elif command == 'undelete': - argument = sys.argv[2].lower() - if argument == 'user': - doUndeleteUser() - elif argument in ['matter', 'vaultmatter']: - gapi.vault.updateMatter(action=command) - elif argument == 'alert': - doDeleteOrUndeleteAlert('undelete') - else: - controlflow.invalid_argument_exit(argument, "gam undelete") - sys.exit(0) - elif command in ['close', 'reopen']: - # close and reopen will have to be split apart if either takes a new argument - argument = sys.argv[2].lower() - if argument in ['matter', 'vaultmatter']: - gapi.vault.updateMatter(action=command) - else: - controlflow.invalid_argument_exit(argument, f"gam {command}") - sys.exit(0) - elif command == 'print': - argument = sys.argv[2].lower().replace('-', '') - if argument == 'users': - doPrintUsers() - elif argument in ['nicknames', 'aliases']: - doPrintAliases() - elif argument == 'groups': - doPrintGroups() - elif argument in ['groupmembers', 'groupsmembers']: - doPrintGroupMembers() - elif argument in ['orgs', 'ous']: - doPrintOrgs() - elif argument == 'resources': - gapi.directory.resource.printResourceCalendars() - elif argument == 'cros': - gapi.directory.cros.doPrintCrosDevices() - elif argument == 'crosactivity': - gapi.directory.cros.doPrintCrosActivity() - elif argument == 'mobile': - doPrintMobileDevices() - elif argument in ['license', 'licenses', 'licence', 'licences']: - doPrintLicenses() - elif argument in ['token', 'tokens', 'oauth', '3lo']: - printShowTokens(3, None, None, True) - elif argument in ['schema', 'schemas']: - doPrintShowUserSchemas(True) - elif argument in ['courses', 'classes']: - doPrintCourses() - elif argument in ['courseparticipants', 'classparticipants']: - doPrintCourseParticipants() - elif argument == 'printers': - doPrintPrinters() - elif argument == 'printjobs': - doPrintPrintJobs() - elif argument in ['transfers', 'datatransfers']: - doPrintDataTransfers() - elif argument == 'transferapps': - doPrintTransferApps() - elif argument == 'domains': - doPrintDomains() - elif argument in ['domainaliases', 'aliasdomains']: - doPrintDomainAliases() - elif argument == 'admins': - doPrintAdmins() - elif argument in ['roles', 'adminroles']: - doPrintAdminRoles() - elif argument in ['guardian', 'guardians']: - doPrintShowGuardians(True) - elif argument in ['matters', 'vaultmatters']: - gapi.vault.printMatters() - elif argument in ['holds', 'vaultholds']: - gapi.vault.printHolds() - elif argument in ['exports', 'vaultexports']: - gapi.vault.printExports() - elif argument in ['building', 'buildings']: - gapi.directory.resource.printBuildings() - elif argument in ['feature', 'features']: - gapi.directory.resource.printFeatures() - elif argument in ['project', 'projects']: - doPrintShowProjects(True) - elif argument in ['alert', 'alerts']: - doPrintShowAlerts() - elif argument in ['alertfeedback', 'alertsfeedback']: - doPrintShowAlertFeedback() - else: - controlflow.invalid_argument_exit(argument, "gam print") - sys.exit(0) - elif command == 'show': - argument = sys.argv[2].lower() - if argument in ['schema', 'schemas']: - doPrintShowUserSchemas(False) - elif argument in ['guardian', 'guardians']: - doPrintShowGuardians(False) - elif argument in ['license', 'licenses', 'licence', 'licences']: - doShowLicenses() - elif argument in ['project', 'projects']: - doPrintShowProjects(False) - elif argument in ['sakey', 'sakeys']: - doShowServiceAccountKeys() - else: - controlflow.invalid_argument_exit(argument, "gam show") - sys.exit(0) - elif command in ['oauth', 'oauth2']: - argument = sys.argv[2].lower() - if argument in ['request', 'create']: - try: - login_hint = sys.argv[3].strip() - except IndexError: - login_hint = None - doRequestOAuth(login_hint) - elif argument in ['info', 'verify']: - OAuthInfo() - elif argument in ['delete', 'revoke']: - doDeleteOAuth() - elif argument in ['refresh']: - creds = getValidOauth2TxtCredentials(force_refresh=True) - if not creds: - controlflow.system_error_exit(5, 'Credential refresh failed') - else: - print('Credentials refreshed') - else: - controlflow.invalid_argument_exit(argument, "gam oauth") - sys.exit(0) - elif command == 'calendar': - argument = sys.argv[3].lower() - if argument == 'showacl': - gapi.calendar.printShowACLs(False) - elif argument == 'printacl': - gapi.calendar.printShowACLs(True) - elif argument == 'add': - gapi.calendar.addACL('Add') - elif argument in ['del', 'delete']: - gapi.calendar.delACL() - elif argument == 'update': - gapi.calendar.addACL('Update') - elif argument == 'wipe': - gapi.calendar.wipeData() - elif argument == 'addevent': - gapi.calendar.addOrUpdateEvent('add') - elif argument == 'updateevent': - gapi.calendar.addOrUpdateEvent('update') - elif argument == 'infoevent': - gapi.calendar.infoEvent() - elif argument == 'deleteevent': - gapi.calendar.moveOrDeleteEvent('delete') - elif argument == 'moveevent': - gapi.calendar.moveOrDeleteEvent('move') - elif argument == 'printevents': - gapi.calendar.printEvents() - elif argument == 'modify': - gapi.calendar.modifySettings() - else: - controlflow.invalid_argument_exit(argument, "gam calendar") - sys.exit(0) - elif command == 'printer': - if sys.argv[2].lower() == 'register': - doPrinterRegister() - sys.exit(0) - argument = sys.argv[3].lower() - if argument == 'showacl': - doPrinterShowACL() - elif argument == 'add': - doPrinterAddACL() - elif argument in ['del', 'delete', 'remove']: - doPrinterDelACL() - else: - controlflow.invalid_argument_exit(argument, "gam printer...") - sys.exit(0) - elif command == 'printjob': - argument = sys.argv[3].lower() - if argument == 'delete': - doDeletePrintJob() - elif argument == 'cancel': - doCancelPrintJob() - elif argument == 'submit': - doPrintJobSubmit() - elif argument == 'fetch': - doPrintJobFetch() - elif argument == 'resubmit': - doPrintJobResubmit() - else: - controlflow.invalid_argument_exit(argument, "gam printjob") - sys.exit(0) - elif command == 'report': - gapi.reports.showReport() - sys.exit(0) - elif command == 'whatis': - doWhatIs() - sys.exit(0) - elif command in ['course', 'class']: - argument = sys.argv[3].lower() - if argument in ['add', 'create']: - doAddCourseParticipant() - elif argument in ['del', 'delete', 'remove']: - doDelCourseParticipant() - elif argument == 'sync': - doSyncCourseParticipants() - else: - controlflow.invalid_argument_exit(argument, "gam course") - sys.exit(0) - elif command == 'download': - argument = sys.argv[2].lower() - if argument in ['export', 'vaultexport']: - gapi.vault.downloadExport() - elif argument in ['storagebucket']: - gapi.storage.download_bucket() - else: - controlflow.invalid_argument_exit(argument, "gam download") - sys.exit(0) - elif command == 'rotate': - argument = sys.argv[2].lower() - if argument in ['sakey', 'sakeys']: - doCreateOrRotateServiceAccountKeys() - else: - controlflow.invalid_argument_exit(argument, "gam rotate") - sys.exit(0) - users = getUsersToModify() - command = sys.argv[3].lower() - if command == 'print' and len(sys.argv) == 4: - for user in users: - print(user) - sys.exit(0) - if (GC_Values[GC_AUTO_BATCH_MIN] > 0) and (len(users) > GC_Values[GC_AUTO_BATCH_MIN]): - runCmdForUsers(None, users, True) - if command == 'transfer': - transferWhat = sys.argv[4].lower() - if transferWhat == 'drive': - transferDriveFiles(users) - elif transferWhat == 'seccals': - gapi.calendar.transferSecCals(users) - else: - controlflow.invalid_argument_exit(transferWhat, "gam transfer") - elif command == 'show': - showWhat = sys.argv[4].lower() - if showWhat in ['labels', 'label']: - showLabels(users) - elif showWhat == 'profile': - showProfile(users) - elif showWhat == 'calendars': - gapi.calendar.printShowCalendars(users, False) - elif showWhat == 'calsettings': - gapi.calendar.showCalSettings(users) - elif showWhat == 'drivesettings': - printDriveSettings(users) - elif showWhat == 'teamdrivethemes': - getTeamDriveThemes(users) - elif showWhat == 'drivefileacl': - showDriveFileACL(users) - elif showWhat == 'filelist': - printDriveFileList(users) - elif showWhat == 'filetree': - showDriveFileTree(users) - elif showWhat == 'fileinfo': - showDriveFileInfo(users) - elif showWhat == 'filerevisions': - showDriveFileRevisions(users) - elif showWhat == 'sendas': - printShowSendAs(users, False) - elif showWhat == 'smime': - printShowSmime(users, False) - elif showWhat == 'gmailprofile': - showGmailProfile(users) - elif showWhat in ['sig', 'signature']: - getSignature(users) - elif showWhat == 'forward': - printShowForward(users, False) - elif showWhat in ['pop', 'pop3']: - getPop(users) - elif showWhat in ['imap', 'imap4']: - getImap(users) - elif showWhat in ['language']: - getLanguage(users) - elif showWhat == 'vacation': - getVacation(users) - elif showWhat in ['delegate', 'delegates']: - printShowDelegates(users, False) - elif showWhat in ['backupcode', 'backupcodes', 'verificationcodes']: - doGetBackupCodes(users) - elif showWhat in ['asp', 'asps', 'applicationspecificpasswords']: - doGetASPs(users) - elif showWhat in ['token', 'tokens', 'oauth', '3lo']: - printShowTokens(5, 'users', users, False) - elif showWhat == 'driveactivity': - printDriveActivity(users) - elif showWhat in ['filter', 'filters']: - printShowFilters(users, False) - elif showWhat in ['forwardingaddress', 'forwardingaddresses']: - printShowForwardingAddresses(users, False) - elif showWhat in ['teamdrive', 'teamdrives']: - printShowTeamDrives(users, False) - elif showWhat in ['teamdriveinfo']: - doGetTeamDriveInfo(users) - else: - controlflow.invalid_argument_exit(showWhat, "gam show") - elif command == 'print': - printWhat = sys.argv[4].lower() - if printWhat == 'calendars': - gapi.calendar.printShowCalendars(users, True) - elif printWhat in ['delegate', 'delegates']: - printShowDelegates(users, True) - elif printWhat == 'driveactivity': - printDriveActivity(users) - elif printWhat == 'drivesettings': - printDriveSettings(users) - elif printWhat == 'filelist': - printDriveFileList(users) - elif printWhat in ['filter', 'filters']: - printShowFilters(users, True) - elif printWhat == 'forward': - printShowForward(users, True) - elif printWhat in ['forwardingaddress', 'forwardingaddresses']: - printShowForwardingAddresses(users, True) - elif printWhat == 'sendas': - printShowSendAs(users, True) - elif printWhat == 'smime': - printShowSmime(users, True) - elif printWhat in ['token', 'tokens', 'oauth', '3lo']: - printShowTokens(5, 'users', users, True) - elif printWhat in ['teamdrive', 'teamdrives']: - printShowTeamDrives(users, True) - else: - controlflow.invalid_argument_exit(printWhat, "gam print") - elif command == 'modify': - modifyWhat = sys.argv[4].lower() - if modifyWhat in ['message', 'messages']: - doProcessMessagesOrThreads(users, 'modify', 'messages') - elif modifyWhat in ['thread', 'threads']: - doProcessMessagesOrThreads(users, 'modify', 'threads') - else: - controlflow.invalid_argument_exit(modifyWhat, "gam modify") - elif command == 'trash': - trashWhat = sys.argv[4].lower() - if trashWhat in ['message', 'messages']: - doProcessMessagesOrThreads(users, 'trash', 'messages') - elif trashWhat in ['thread', 'threads']: - doProcessMessagesOrThreads(users, 'trash', 'threads') - else: - controlflow.invalid_argument_exit(trashWhat, "gam trash") - elif command == 'untrash': - untrashWhat = sys.argv[4].lower() - if untrashWhat in ['message', 'messages']: - doProcessMessagesOrThreads(users, 'untrash', 'messages') - elif untrashWhat in ['thread', 'threads']: - doProcessMessagesOrThreads(users, 'untrash', 'threads') - else: - controlflow.invalid_argument_exit(untrashWhat, "gam untrash") - elif command in ['delete', 'del']: - delWhat = sys.argv[4].lower() - if delWhat == 'delegate': - deleteDelegate(users) - elif delWhat == 'calendar': - gapi.calendar.deleteCalendar(users) - elif delWhat in ['labels', 'label']: - doDeleteLabel(users) - elif delWhat in ['message', 'messages']: - runCmdForUsers(doProcessMessagesOrThreads, users, default_to_batch=True, function='delete', unit='messages') - elif delWhat in ['thread', 'threads']: - runCmdForUsers(doProcessMessagesOrThreads, users, default_to_batch=True, function='delete', unit='threads') - elif delWhat == 'photo': - deletePhoto(users) - elif delWhat in ['license', 'licence']: - doLicense(users, 'delete') - elif delWhat in ['backupcode', 'backupcodes', 'verificationcodes']: - doDelBackupCodes(users) - elif delWhat in ['asp', 'asps', 'applicationspecificpasswords']: - doDelASP(users) - elif delWhat in ['token', 'tokens', 'oauth', '3lo']: - doDelTokens(users) - elif delWhat in ['group', 'groups']: - deleteUserFromGroups(users) - elif delWhat in ['alias', 'aliases']: - doRemoveUsersAliases(users) - elif delWhat == 'emptydrivefolders': - deleteEmptyDriveFolders(users) - elif delWhat == 'drivefile': - deleteDriveFile(users) - elif delWhat in ['drivefileacl', 'drivefileacls']: - delDriveFileACL(users) - elif delWhat in ['filter', 'filters']: - deleteFilters(users) - elif delWhat in ['forwardingaddress', 'forwardingaddresses']: - deleteForwardingAddresses(users) - elif delWhat == 'sendas': - deleteSendAs(users) - elif delWhat == 'smime': - deleteSmime(users) - elif delWhat == 'teamdrive': - doDeleteTeamDrive(users) - else: - controlflow.invalid_argument_exit(delWhat, "gam delete") - elif command in ['add', 'create']: - addWhat = sys.argv[4].lower() - if addWhat == 'calendar': - if command == 'add': - gapi.calendar.addCalendar(users) - else: - controlflow.system_error_exit(2, f'{addWhat} is not implemented for "gam {command}"') - elif addWhat == 'drivefile': - createDriveFile(users) - elif addWhat in ['license', 'licence']: - doLicense(users, 'insert') - elif addWhat in ['drivefileacl', 'drivefileacls']: - addDriveFileACL(users) - elif addWhat in ['label', 'labels']: - doLabel(users, 5) - elif addWhat in ['delegate', 'delegates']: - addDelegates(users, 5) - elif addWhat in ['filter', 'filters']: - addFilter(users, 5) - elif addWhat in ['forwardingaddress', 'forwardingaddresses']: - addForwardingAddresses(users) - elif addWhat == 'sendas': - addUpdateSendAs(users, 5, True) - elif addWhat == 'smime': - addSmime(users) - elif addWhat == 'teamdrive': - doCreateTeamDrive(users) - else: - controlflow.invalid_argument_exit(addWhat, f"gam {command}") - elif command == 'update': - updateWhat = sys.argv[4].lower() - if updateWhat == 'calendar': - gapi.calendar.updateCalendar(users) - elif updateWhat == 'calattendees': - gapi.calendar.changeAttendees(users) - elif updateWhat == 'photo': - doPhoto(users) - elif updateWhat in ['license', 'licence']: - doLicense(users, 'patch') - elif updateWhat == 'user': - doUpdateUser(users, 5) - elif updateWhat in ['backupcode', 'backupcodes', 'verificationcodes']: - doGenBackupCodes(users) - elif updateWhat == 'drivefile': - doUpdateDriveFile(users) - elif updateWhat in ['drivefileacls', 'drivefileacl']: - updateDriveFileACL(users) - elif updateWhat in ['label', 'labels']: - renameLabels(users) - elif updateWhat == 'labelsettings': - updateLabels(users) - elif updateWhat == 'sendas': - addUpdateSendAs(users, 5, False) - elif updateWhat == 'smime': - updateSmime(users) - elif updateWhat == 'teamdrive': - doUpdateTeamDrive(users) - else: - controlflow.invalid_argument_exit(updateWhat, "gam update") - elif command in ['deprov', 'deprovision']: - doDeprovUser(users) - elif command == 'get': - getWhat = sys.argv[4].lower() - if getWhat == 'photo': - getPhoto(users) - elif getWhat == 'drivefile': - downloadDriveFile(users) - else: - controlflow.invalid_argument_exit(getWhat, "gam get") - elif command == 'empty': - emptyWhat = sys.argv[4].lower() - if emptyWhat == 'drivetrash': - doEmptyDriveTrash(users) - else: - controlflow.invalid_argument_exit(emptyWhat, "gam empty") - elif command == 'info': - infoWhat = sys.argv[4].lower() - if infoWhat == 'calendar': - gapi.calendar.infoCalendar(users) - elif infoWhat in ['filter', 'filters']: - infoFilters(users) - elif infoWhat in ['forwardingaddress', 'forwardingaddresses']: - infoForwardingAddresses(users) - elif infoWhat == 'sendas': - infoSendAs(users) - else: - controlflow.invalid_argument_exit(infoWhat, "gam info") - elif command == 'check': - checkWhat = sys.argv[4].replace('_', '').lower() - if checkWhat == 'serviceaccount': - doCheckServiceAccount(users) - else: - controlflow.invalid_argument_exit(checkWhat, "gam check") - elif command == 'profile': - doProfile(users) - elif command == 'imap': - #doImap(users) - runCmdForUsers(doImap, users, default_to_batch=True) - elif command == 'sendemail': - sendOrDropEmail(users, 'send') - elif command == 'importemail': - sendOrDropEmail(users, 'import') - elif command == 'insertemail': - sendOrDropEmail(users, 'insert') - elif command == 'draftemail': - sendOrDropEmail(users, 'draft') - elif command == 'language': - doLanguage(users) - elif command in ['pop', 'pop3']: - doPop(users) - elif command == 'sendas': - addUpdateSendAs(users, 4, True) - elif command == 'label': - doLabel(users, 4) - elif command == 'filter': - addFilter(users, 4) - elif command == 'forward': - doForward(users) - elif command in ['sig', 'signature']: - doSignature(users) - elif command == 'vacation': - doVacation(users) - elif command in ['delegate', 'delegates']: - addDelegates(users, 4) - elif command == 'watch': - if len(sys.argv) > 4: - watchWhat = sys.argv[4].lower() - else: - watchWhat = 'gmail' - if watchWhat == 'gmail': - watchGmail(users) - else: - controlflow.invalid_argument_exit(watchWhat, "gam watch") - else: - controlflow.invalid_argument_exit(command, "gam") - except IndexError: - showUsage() - sys.exit(2) - except KeyboardInterrupt: - sys.exit(50) - except socket.error as e: - controlflow.system_error_exit(3, str(e)) - except MemoryError: - controlflow.system_error_exit(99, MESSAGE_GAM_OUT_OF_MEMORY) - except SystemExit as e: - GM_Globals[GM_SYSEXITRC] = e.code - return GM_Globals[GM_SYSEXITRC] +from gam.__main__ import main # Run from command line if __name__ == "__main__": - mp_freeze_support() - if sys.platform == 'darwin': - # https://bugs.python.org/issue33725 in Python 3.8.0 seems - # to break parallel operations with errors about extra -b - # command line arguments - mp_set_start_method('fork') - if sys.version_info[0] < 3 or sys.version_info[1] < 6: - controlflow.system_error_exit(5, f'GAM requires Python 3.6 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.' % sys.version_info[:3]) - sys.exit(ProcessGAMCommand(sys.argv)) + main(sys.argv) \ No newline at end of file diff --git a/src/gam.spec b/src/gam.spec index 0451f460..5b1894cf 100644 --- a/src/gam.spec +++ b/src/gam.spec @@ -15,7 +15,7 @@ extra_files += [(os.path.join(proot, 'cacerts.txt'), 'httplib2')] extra_files += copy_metadata('google-api-python-client') -a = Analysis(['gam.py'], +a = Analysis(['gam/__main__.py'], hiddenimports=[], hookspath=None, excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'], diff --git a/src/gam/__init__.py b/src/gam/__init__.py new file mode 100755 index 00000000..7a9d8c19 --- /dev/null +++ b/src/gam/__init__.py @@ -0,0 +1,11576 @@ +"""Main behavioral methods and argument routing for GAM.""" + +import base64 +import configparser +import csv +import datetime +import difflib +from email import message_from_string +import hashlib +import io +import json +import mimetypes +import os +import pkg_resources +import platform +from pathlib import Path +import random +from secrets import SystemRandom +import re +import shlex +import signal +import socket +import ssl +import struct +import sys +import time +import uuid +import webbrowser +import zipfile +import http.client as http_client +from multiprocessing import Pool as mp_pool +from urllib.parse import quote, urlencode, urlparse +import dateutil.parser + +import googleapiclient +import googleapiclient.discovery +import googleapiclient.errors +import googleapiclient.http +import google.oauth2.service_account +import httplib2 + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +import gam.auth.oauth +from gam import auth +from gam import controlflow +from gam import display +from gam import fileutils +from gam.gapi import calendar as gapi_calendar +from gam.gapi import directory as gapi_directory +from gam.gapi.directory import cros as gapi_directory_cros +from gam.gapi.directory import customer as gapi_directory_customer +from gam.gapi.directory import resource as gapi_directory_resource +from gam.gapi import errors as gapi_errors +from gam.gapi import reports as gapi_reports +from gam.gapi import storage as gapi_storage +from gam.gapi import vault as gapi_vault +from gam import gapi +from gam import transport +from gam import utils +from gam.var import * + +if platform.system() == 'Windows': + # No crypt module on Win, use passlib + from passlib.hash import sha512_crypt +else: + from crypt import crypt + +if platform.system() == 'Linux': + import distro + +# Finding path method varies between Python source, PyInstaller and StaticX +if os.environ.get('STATICX_PROG_PATH', False): + # StaticX static executable + GM_Globals[GM_GAM_PATH] = os.path.dirname(os.environ['STATICX_PROG_PATH']) + GM_Globals[GM_GAM_TYPE] = 'staticx' + # Pyinstaller executable +elif getattr(sys, 'frozen', False): + GM_Globals[GM_GAM_PATH] = os.path.dirname(sys.executable) + GM_Globals[GM_GAM_TYPE] = 'pyinstaller' +else: + # Source code + GM_Globals[GM_GAM_PATH] = os.path.dirname(Path(os.path.realpath(__file__)).parent) + GM_Globals[GM_GAM_TYPE] = 'pythonsource' + +def showUsage(): + doGAMVersion(checkForArgs=False) + print(''' +Usage: gam [OPTIONS]... + +GAM. Retrieve or set G Suite domain, +user, group and alias settings. Exhaustive list of commands +can be found at: https://github.com/jay0lee/GAM/wiki + +Examples: +gam info domain +gam create user jsmith firstname John lastname Smith password secretpass +gam update user jsmith suspended on +gam.exe update group announcements add member jsmith +... + +''') + +def currentCount(i, count): + return f' ({i}/{count})' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '' + +def currentCountNL(i, count): + return f' ({i}/{count})\n' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '\n' + +def printGettingAllItems(items, query): + if query: + sys.stderr.write(f"Getting all {items} in G Suite account that match query ({query}) (may take some time on a large account)...\n") + else: + sys.stderr.write(f"Getting all {items} in G Suite account (may take some time on a large account)...\n") + +def entityServiceNotApplicableWarning(entityType, entityName, i, count): + sys.stderr.write(f'{entityType}: {entityName}, Service not applicable/Does not exist{currentCountNL(i, count)}') + +def entityDoesNotExistWarning(entityType, entityName, i, count): + sys.stderr.write(f'{entityType}: {entityName}, Does not exist{currentCountNL(i, count)}') + +def entityUnknownWarning(entityType, entityName, i, count): + domain = getEmailAddressDomain(entityName) + if (domain == GC_Values[GC_DOMAIN]) or (domain.endswith('google.com')): + entityDoesNotExistWarning(entityType, entityName, i, count) + else: + entityServiceNotApplicableWarning(entityType, entityName, i, count) + +def printLine(message): + sys.stdout.write(message+'\n') + +def getBoolean(value, item): + value = value.lower() + if value in true_values: + return True + if value in false_values: + return False + controlflow.system_error_exit(2, f'Value for {item} must be {"|".join(true_values)} or {"|".join(false_values)}; got {value}') + +def getCharSet(i): + if (i == len(sys.argv)) or (sys.argv[i].lower() != 'charset'): + return (i, GC_Values.get(GC_CHARSET, GM_Globals[GM_SYS_ENCODING])) + return (i+2, sys.argv[i+1]) + +def supportsColoredText(): + """Determines if the current terminal environment supports colored text. + + Returns: + Bool, True if the current terminal environment supports colored text via + ANSI escape characters. + """ + # Make a rudimentary check for Windows. Though Windows does seem to support + # colorization with VT100 emulation, it is disabled by default. Therefore, + # we'll simply disable it in GAM on Windows for now. + return not GM_Globals[GM_WINDOWS] + +def createColoredText(text, color): + """Uses ANSI escape characters to create colored text in supported terminals. + + See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + + Args: + text: String, The text to colorize using ANSI escape characters. + color: String, An ANSI escape sequence denoting the color of the text to be + created. See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + + Returns: + The input text with appropriate ANSI escape characters to create + colorization in a supported terminal environment. + """ + END_COLOR_SEQUENCE = '\033[0m' # Ends the applied color formatting + if supportsColoredText(): + return color + text + END_COLOR_SEQUENCE + return text # Hand back the plain text, uncolorized. + +def createRedText(text): + """Uses ANSI encoding to create red colored text if supported.""" + return createColoredText(text, '\033[91m') + +def createGreenText(text): + """Uses ANSI encoding to create green colored text if supported.""" + return createColoredText(text, '\u001b[32m') + +def createYellowText(text): + """Uses ANSI encoding to create yellow text if supported.""" + return createColoredText(text, '\u001b[33m') + +COLORHEX_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$') + +def getColor(color): + color = color.lower().strip() + if color in WEBCOLOR_MAP: + return WEBCOLOR_MAP[color] + tg = COLORHEX_PATTERN.match(color) + if tg: + return tg.group(0) + controlflow.system_error_exit(2, f'A color must be a valid name or # and six hex characters (#012345); got {color}') + +def getLabelColor(color): + color = color.lower().strip() + tg = COLORHEX_PATTERN.match(color) + if tg: + color = tg.group(0) + if color in LABEL_COLORS: + return color + controlflow.expected_argument_exit("label color", ", ".join(LABEL_COLORS), color) + controlflow.system_error_exit(2, f'A label color must be # and six hex characters (#012345); got {color}') + +def getInteger(value, item, minVal=None, maxVal=None): + try: + number = int(value.strip()) + if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)): + return number + except ValueError: + pass + controlflow.system_error_exit(2, f'expected {item} in range <{utils.integerLimits(minVal, maxVal)}>, got {value}') + +def removeCourseIdScope(courseId): + if courseId.startswith('d:'): + return courseId[2:] + return courseId + +def addCourseIdScope(courseId): + if not courseId.isdigit() and courseId[:2] != 'd:': + return f'd:{courseId}' + return courseId + +# Get domain from email address +def getEmailAddressDomain(emailAddress): + atLoc = emailAddress.find('@') + if atLoc == -1: + return GC_Values[GC_DOMAIN].lower() + return emailAddress[atLoc+1:].lower() + +# Split email address unto user and domain +def splitEmailAddress(emailAddress): + atLoc = emailAddress.find('@') + if atLoc == -1: + return (emailAddress.lower(), GC_Values[GC_DOMAIN].lower()) + return (emailAddress[:atLoc].lower(), emailAddress[atLoc+1:].lower()) + +# Normalize user/group email address/uid +# uid:12345abc -> 12345abc +# foo -> foo@domain +# foo@ -> foo@domain +# foo@bar.com -> foo@bar.com +# @domain -> domain +def normalizeEmailAddressOrUID(emailAddressOrUID, noUid=False, checkForCustomerId=False, noLower=False): + if checkForCustomerId and (emailAddressOrUID == GC_Values[GC_CUSTOMER_ID]): + return emailAddressOrUID + if not noUid: + cg = UID_PATTERN.match(emailAddressOrUID) + if cg: + return cg.group(1) + atLoc = emailAddressOrUID.find('@') + if atLoc == 0: + return emailAddressOrUID[1:].lower() if not noLower else emailAddressOrUID[1:] + if (atLoc == -1) or (atLoc == len(emailAddressOrUID)-1) and GC_Values[GC_DOMAIN]: + if atLoc == -1: + emailAddressOrUID = f'{emailAddressOrUID}@{GC_Values[GC_DOMAIN]}' + else: + emailAddressOrUID = f'{emailAddressOrUID}{GC_Values[GC_DOMAIN]}' + return emailAddressOrUID.lower() if not noLower else emailAddressOrUID + +# Normalize student/guardian email address/uid +# 12345678 -> 12345678 +# - -> - +# Otherwise, same results as normalizeEmailAddressOrUID +def normalizeStudentGuardianEmailAddressOrUID(emailAddressOrUID): + if emailAddressOrUID.isdigit() or emailAddressOrUID == '-': + return emailAddressOrUID + return normalizeEmailAddressOrUID(emailAddressOrUID) +# +# Set global variables +# Check for GAM updates based on status of noupdatecheck.txt +# +def SetGlobalVariables(): + + def _getOldEnvVar(itemName, envVar): + value = os.environ.get(envVar, GC_Defaults[itemName]) + if GC_VAR_INFO[itemName][GC_VAR_TYPE] == GC_TYPE_INTEGER: + try: + number = int(value) + minVal, maxVal = GC_VAR_INFO[itemName][GC_VAR_LIMITS] + if number < minVal: + number = minVal + elif maxVal and (number > maxVal): + number = maxVal + except ValueError: + number = GC_Defaults[itemName] + value = number + GC_Defaults[itemName] = value + + def _getOldSignalFile(itemName, fileName, filePresentValue=True, fileAbsentValue=False): + GC_Defaults[itemName] = filePresentValue if os.path.isfile(os.path.join(GC_Defaults[GC_CONFIG_DIR], fileName)) else fileAbsentValue + + def _getCfgDirectory(itemName): + return GC_Defaults[itemName] + + def _getCfgFile(itemName): + if not GC_Defaults[itemName]: + return None + value = os.path.expanduser(GC_Defaults[itemName]) + if not os.path.isabs(value): + value = os.path.expanduser(os.path.join(GC_Values[GC_CONFIG_DIR], value)) + return value + + def _getCfgHeaderFilter(itemName): + value = GC_Defaults[itemName] + headerFilters = [] + if not value: + return headerFilters + filters = shlexSplitList(value) + for filterStr in filters: + try: + headerFilters.append(re.compile(filterStr, re.IGNORECASE)) + except re.error as e: + controlflow.system_error_exit(3, f'Item: {itemName}: "{filterStr}", Invalid RE: {str(e)}') + return headerFilters + + ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count)\s*([<>]=?|=|!=)\s*(.+)$', re.IGNORECASE) + ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE) + ROW_FILTER_RE_PATTERN = re.compile(r'^(regex|notregex):(.+)$', re.IGNORECASE) + + def _getCfgRowFilter(itemName): + value = GC_Defaults[itemName] + rowFilters = {} + if not value: + return rowFilters + if value.startswith('{'): + try: + filterDict = json.loads(value.encode('unicode-escape').decode(UTF8)) + except (TypeError, ValueError) as e: + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{value}", Failed to parse as JSON: {str(e)}') + else: + filterDict = {} + status, filterList = shlexSplitListStatus(value) + if not status: + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{value}", Failed to parse as list') + for filterVal in filterList: + if not filterVal: + continue + try: + filterTokens = shlexSplitList(filterVal, ':') + column = filterTokens[0] + filterStr = ':'.join(filterTokens[1:]) + except ValueError: + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{filterVal}", Expected column:filter') + filterDict[column] = filterStr + for column, filterStr in iter(filterDict.items()): + mg = ROW_FILTER_COMP_PATTERN.match(filterStr) + if mg: + if mg.group(1) in ['date', 'time']: + if mg.group(1) == 'date': + valid, filterValue = utils.get_row_filter_date_or_delta_from_now(mg.group(3)) + else: + valid, filterValue = utils.get_row_filter_time_or_delta_from_now(mg.group(3)) + if valid: + rowFilters[column] = (mg.group(1), mg.group(2), filterValue) + continue + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Expected: {filterValue}') + else: #count + if mg.group(3).isdigit(): + rowFilters[column] = (mg.group(1), mg.group(2), int(mg.group(3))) + continue + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Expected: ') + mg = ROW_FILTER_BOOL_PATTERN.match(filterStr) + if mg: + value = mg.group(2).lower() + if value in true_values: + filterValue = True + elif value in false_values: + filterValue = False + else: + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Expected true|false') + rowFilters[column] = (mg.group(1), filterValue) + continue + mg = ROW_FILTER_RE_PATTERN.match(filterStr) + if mg: + try: + rowFilters[column] = (mg.group(1), re.compile(mg.group(2))) + continue + except re.error as e: + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": "{filterStr}", Invalid RE: {str(e)}') + controlflow.system_error_exit(3, f'Item: {itemName}, Value: "{column}": {filterStr}, Expected: (date|time|count) or (boolean:true|false) or (regex|notregex:)') + return rowFilters + + GC_Defaults[GC_CONFIG_DIR] = GM_Globals[GM_GAM_PATH] + GC_Defaults[GC_CACHE_DIR] = os.path.join(GM_Globals[GM_GAM_PATH], 'gamcache') + GC_Defaults[GC_DRIVE_DIR] = GM_Globals[GM_GAM_PATH] + GC_Defaults[GC_SITE_DIR] = GM_Globals[GM_GAM_PATH] + + _getOldEnvVar(GC_CONFIG_DIR, 'GAMUSERCONFIGDIR') + _getOldEnvVar(GC_SITE_DIR, 'GAMSITECONFIGDIR') + _getOldEnvVar(GC_CACHE_DIR, 'GAMCACHEDIR') + _getOldEnvVar(GC_DRIVE_DIR, 'GAMDRIVEDIR') + _getOldEnvVar(GC_OAUTH2_TXT, 'OAUTHFILE') + _getOldEnvVar(GC_OAUTH2SERVICE_JSON, 'OAUTHSERVICEFILE') + if GC_Defaults[GC_OAUTH2SERVICE_JSON].find('.') == -1: + GC_Defaults[GC_OAUTH2SERVICE_JSON] += '.json' + _getOldEnvVar(GC_CLIENT_SECRETS_JSON, 'CLIENTSECRETS') + _getOldEnvVar(GC_DOMAIN, 'GA_DOMAIN') + _getOldEnvVar(GC_CUSTOMER_ID, 'CUSTOMER_ID') + _getOldEnvVar(GC_CHARSET, 'GAM_CHARSET') + _getOldEnvVar(GC_NUM_THREADS, 'GAM_THREADS') + _getOldEnvVar(GC_AUTO_BATCH_MIN, 'GAM_AUTOBATCH') + _getOldEnvVar(GC_BATCH_SIZE, 'GAM_BATCH_SIZE') + _getOldEnvVar(GC_CSV_HEADER_FILTER, 'GAM_CSV_HEADER_FILTER') + _getOldEnvVar(GC_CSV_HEADER_DROP_FILTER, 'GAM_CSV_HEADER_DROP_FILTER') + _getOldEnvVar(GC_CSV_ROW_FILTER, 'GAM_CSV_ROW_FILTER') + _getOldEnvVar(GC_TLS_MIN_VERSION, 'GAM_TLS_MIN_VERSION') + _getOldEnvVar(GC_TLS_MAX_VERSION, 'GAM_TLS_MAX_VERSION') + _getOldEnvVar(GC_CA_FILE, 'GAM_CA_FILE') + _getOldSignalFile(GC_DEBUG_LEVEL, 'debug.gam', filePresentValue=4, fileAbsentValue=0) + _getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt') + _getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt') +# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt') +# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True) + _getOldSignalFile(GC_NO_CACHE, 'allcache.txt', filePresentValue=False, fileAbsentValue=True) + _getOldSignalFile(GC_NO_UPDATE_CHECK, 'noupdatecheck.txt') +# Assign directories first + for itemName in GC_VAR_INFO: + if GC_VAR_INFO[itemName][GC_VAR_TYPE] == GC_TYPE_DIRECTORY: + GC_Values[itemName] = _getCfgDirectory(itemName) + for itemName in GC_VAR_INFO: + varType = GC_VAR_INFO[itemName][GC_VAR_TYPE] + if varType == GC_TYPE_FILE: + GC_Values[itemName] = _getCfgFile(itemName) + elif varType == GC_TYPE_HEADERFILTER: + GC_Values[itemName] = _getCfgHeaderFilter(itemName) + elif varType == GC_TYPE_ROWFILTER: + GC_Values[itemName] = _getCfgRowFilter(itemName) + else: + GC_Values[itemName] = GC_Defaults[itemName] + GM_Globals[GM_LAST_UPDATE_CHECK_TXT] = os.path.join(GC_Values[GC_CONFIG_DIR], FN_LAST_UPDATE_CHECK_TXT) + if not GC_Values[GC_NO_UPDATE_CHECK]: + doGAMCheckForUpdates() +# Globals derived from config file values + GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = None + GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = None + GM_Globals[GM_EXTRA_ARGS_DICT] = {'prettyPrint': GC_Values[GC_DEBUG_LEVEL] > 0} +# override httplib2 settings + httplib2.debuglevel = GC_Values[GC_DEBUG_LEVEL] + if os.path.isfile(os.path.join(GC_Values[GC_CONFIG_DIR], FN_EXTRA_ARGS_TXT)): + ea_config = configparser.ConfigParser() + ea_config.optionxform = str + ea_config.read(os.path.join(GC_Values[GC_CONFIG_DIR], FN_EXTRA_ARGS_TXT)) + GM_Globals[GM_EXTRA_ARGS_DICT].update(dict(ea_config.items('extra-args'))) + if GC_Values[GC_NO_CACHE]: + GM_Globals[GM_CACHE_DIR] = None + GM_Globals[GM_CACHE_DISCOVERY_ONLY] = False + else: + GM_Globals[GM_CACHE_DIR] = GC_Values[GC_CACHE_DIR] +# GM_Globals[GM_CACHE_DISCOVERY_ONLY] = GC_Values[GC_CACHE_DISCOVERY_ONLY] + GM_Globals[GM_CACHE_DISCOVERY_ONLY] = False + return True + +TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60), ('second', 1)] + +def getLocalGoogleTimeOffset(testLocation='www.googleapis.com'): + localUTC = datetime.datetime.now(datetime.timezone.utc) + try: + # we disable SSL verify so we can still get time even if clock + # is way off. This could be spoofed / MitM but we'll fail for those + # situations everywhere else but here. + badhttp = transport.create_http() + badhttp.disable_ssl_certificate_validation = True + googleUTC = dateutil.parser.parse(badhttp.request('https://'+testLocation, 'HEAD')[0]['date']) + except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e: + controlflow.system_error_exit(4, str(e)) + offset = remainder = int(abs((localUTC-googleUTC).total_seconds())) + timeoff = [] + for tou in TIME_OFFSET_UNITS: + uval, remainder = divmod(remainder, tou[1]) + if uval: + timeoff.append(f'{uval} {tou[0]}{"s" if uval != 1 else ""}') + if not timeoff: + timeoff.append('less than 1 second') + nicetime = ', '.join(timeoff) + return (offset, nicetime) + +def doGAMCheckForUpdates(forceCheck=False): + + def _gamLatestVersionNotAvailable(): + if forceCheck: + controlflow.system_error_exit(4, 'GAM Latest Version information not available') + + current_version = gam_version + now_time = int(time.time()) + if forceCheck: + check_url = GAM_ALL_RELEASES # includes pre-releases + else: + last_check_time_str = fileutils.read_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], continue_on_error=True, display_errors=False) + last_check_time = int(last_check_time_str) if last_check_time_str and last_check_time_str.isdigit() else 0 + if last_check_time > now_time-604800: + return + check_url = GAM_LATEST_RELEASE # latest full release + headers = {'Accept': 'application/vnd.github.v3.text+json'} + simplehttp = transport.create_http(timeout=10) + try: + (_, c) = simplehttp.request(check_url, 'GET', headers=headers) + try: + release_data = json.loads(c.decode(UTF8)) + except ValueError: + _gamLatestVersionNotAvailable() + return + if isinstance(release_data, list): + release_data = release_data[0] # only care about latest release + if not isinstance(release_data, dict) or 'tag_name' not in release_data: + _gamLatestVersionNotAvailable() + return + latest_version = release_data['tag_name'] + if latest_version[0].lower() == 'v': + latest_version = latest_version[1:] + if forceCheck or (latest_version > current_version): + print(f'Version Check:\n Current: {current_version}\n Latest: {latest_version}') + if latest_version <= current_version: + fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], str(now_time), continue_on_error=True, display_errors=forceCheck) + return + announcement = release_data.get('body_text', 'No details about this release') + sys.stderr.write(f'\nGAM {latest_version} release notes:\n\n') + sys.stderr.write(announcement) + try: + printLine(MESSAGE_HIT_CONTROL_C_TO_UPDATE) + time.sleep(15) + except KeyboardInterrupt: + webbrowser.open(release_data['html_url']) + printLine(MESSAGE_GAM_EXITING_FOR_UPDATE) + sys.exit(0) + fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], str(now_time), continue_on_error=True, display_errors=forceCheck) + return + except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError, RuntimeError, socket.timeout): + return + +def getOSPlatform(): + myos = platform.system() + if myos == 'Linux': + pltfrm = ' '.join(distro.linux_distribution(full_distribution_name=False)).title() + elif myos == 'Windows': + pltfrm = ' '.join(platform.win32_ver()) + elif myos == 'Darwin': + myos = 'MacOS' + mac_ver = platform.mac_ver()[0] + minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14 + codename = MACOS_CODENAMES.get(minor_ver, '') + pltfrm = ' '.join([codename, mac_ver]) + else: + pltfrm = platform.platform() + return f'{myos} {pltfrm}' + +def doGAMVersion(checkForArgs=True): + force_check = extended = simple = timeOffset = False + testLocation = 'www.googleapis.com' + if checkForArgs: + i = 2 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'check': + force_check = True + i += 1 + elif myarg == 'simple': + simple = True + i += 1 + elif myarg == 'extended': + extended = True + timeOffset = True + i += 1 + elif myarg == 'timeoffset': + timeOffset = True + i += 1 + elif myarg == 'location': + testLocation = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam version") + if simple: + sys.stdout.write(gam_version) + return + pyversion = platform.python_version() + cpu_bits = struct.calcsize('P') * 8 + api_client_ver = pkg_resources.get_distribution("google-api-python-client").version + print((f'GAM {gam_version} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n' + f'{gam_author}\n' + f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n' + f'google-api-python-client {api_client_ver}\n' + f'{getOSPlatform()} {platform.machine()}\n' + f'Path: {GM_Globals[GM_GAM_PATH]}')) + if sys.platform.startswith('win') and \ + cpu_bits == 32 and \ + platform.machine().find('64') != -1: + print(MESSAGE_UPDATE_GAM_TO_64BIT) + if timeOffset: + offset, nicetime = getLocalGoogleTimeOffset(testLocation) + print(MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY % (testLocation, nicetime)) + if offset > MAX_LOCAL_GOOGLE_TIME_OFFSET: + controlflow.system_error_exit(4, 'Please fix your system time.') + if force_check: + doGAMCheckForUpdates(forceCheck=True) + if extended: + print(ssl.OPENSSL_VERSION) + tls_ver, cipher_name, used_ip = _getServerTLSUsed(testLocation) + print(f'{testLocation} ({used_ip}) connects using {tls_ver} {cipher_name}') + +def _getServerTLSUsed(location): + url = f'https://{location}' + _, netloc, _, _, _, _ = urlparse(url) + conn = f'https:{netloc}' + httpc = transport.create_http() + headers = {'user-agent': GAM_INFO} + retries = 5 + for n in range(1, retries+1): + try: + httpc.request(url, headers=headers) + break + except (httplib2.ServerNotFoundError, RuntimeError) as e: + if n != retries: + httpc.connections = {} + controlflow.wait_on_failure(n, retries, str(e)) + continue + controlflow.system_error_exit(4, str(e)) + cipher_name, tls_ver, _ = httpc.connections[conn].sock.cipher() + used_ip = httpc.connections[conn].sock.getpeername()[0] + return tls_ver, cipher_name, used_ip + +def _getSvcAcctData(): + if not GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]: + json_string = fileutils.read_file(GC_Values[GC_OAUTH2SERVICE_JSON], continue_on_error=True, display_errors=True) + if not json_string: + printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON) + controlflow.system_error_exit(6, None) + GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string) + +def getSvcAcctCredentials(scopes, act_as): + try: + _getSvcAcctData() + credentials = google.oauth2.service_account.Credentials.from_service_account_info(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]) + credentials = credentials.with_scopes(scopes) + if act_as: + credentials = credentials.with_subject(act_as) + GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_id'] + return credentials + except (ValueError, KeyError): + printLine(MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON) + controlflow.invalid_json_exit(GC_Values[GC_OAUTH2SERVICE_JSON]) + +def getAPIVersion(api): + version = API_VER_MAPPING.get(api, 'v1') + if api in ['directory', 'reports', 'datatransfer']: + api = 'admin' + elif api == 'drive3': + api = 'drive' + elif api == 'cloudresourcemanagerv1': + api = 'cloudresourcemanager' + return (api, version, f'{api}-{version}') + +def readDiscoveryFile(api_version): + disc_filename = f'{api_version}.json' + disc_file = os.path.join(GM_Globals[GM_GAM_PATH], disc_filename) + if hasattr(sys, '_MEIPASS'): + pyinstaller_disc_file = os.path.join(sys._MEIPASS, disc_filename) #pylint: disable=no-member + else: + pyinstaller_disc_file = None + if os.path.isfile(disc_file): + json_string = fileutils.read_file(disc_file) + elif pyinstaller_disc_file: + json_string = fileutils.read_file(pyinstaller_disc_file) + else: + controlflow.system_error_exit(11, MESSAGE_NO_DISCOVERY_INFORMATION.format(disc_file)) + try: + discovery = json.loads(json_string) + return (disc_file, discovery) + except ValueError: + controlflow.invalid_json_exit(disc_file) + +def getOauth2TxtStorageCredentials(): + try: + return auth.get_admin_credentials() + except gam.auth.oauth.InvalidCredentialsFileError: + # Maintain legacy behavior of this method that returns None if no + # credential file is present. + return None + +def getValidOauth2TxtCredentials(force_refresh=False): + """Gets OAuth2 credentials which are guaranteed to be fresh and valid.""" + try: + credentials = auth.get_admin_credentials() + except gam.auth.oauth.InvalidCredentialsFileError: + doRequestOAuth() # Make a new request which should store new creds. + return getValidOauth2TxtCredentials(force_refresh=force_refresh) + + if credentials.expired or force_refresh: + request = transport.create_request() + credentials.refresh(request) + return credentials + +def getService(api, http): + api, version, api_version = getAPIVersion(api) + if api in GM_Globals[GM_CURRENT_API_SERVICES] and version in GM_Globals[GM_CURRENT_API_SERVICES][api]: + service = googleapiclient.discovery.build_from_document(GM_Globals[GM_CURRENT_API_SERVICES][api][version], http=http) + if GM_Globals[GM_CACHE_DISCOVERY_ONLY]: + http.cache = None + return service + if api in V1_DISCOVERY_APIS: + discoveryServiceUrl = googleapiclient.discovery.DISCOVERY_URI + else: + discoveryServiceUrl = googleapiclient.discovery.V2_DISCOVERY_URI + retries = 3 + for n in range(1, retries+1): + try: + service = googleapiclient.discovery.build(api, version, http=http, cache_discovery=False, discoveryServiceUrl=discoveryServiceUrl) + GM_Globals[GM_CURRENT_API_SERVICES].setdefault(api, {}) + GM_Globals[GM_CURRENT_API_SERVICES][api][version] = service._rootDesc.copy() + if GM_Globals[GM_CACHE_DISCOVERY_ONLY]: + http.cache = None + return service + except (httplib2.ServerNotFoundError, RuntimeError) as e: + if n != retries: + http.connections = {} + controlflow.wait_on_failure(n, retries, str(e)) + continue + controlflow.system_error_exit(4, str(e)) + except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: + http.cache = None + if n != retries: + controlflow.wait_on_failure(n, retries, str(e)) + continue + controlflow.system_error_exit(17, str(e)) + except (http_client.ResponseNotReady, socket.error, + googleapiclient.errors.HttpError) as e: + if n != retries: + controlflow.wait_on_failure(n, retries, str(e)) + continue + controlflow.system_error_exit(3, str(e)) + except googleapiclient.errors.UnknownApiNameOrVersion: + break + disc_file, discovery = readDiscoveryFile(api_version) + try: + service = googleapiclient.discovery.build_from_document(discovery, http=http) + GM_Globals[GM_CURRENT_API_SERVICES].setdefault(api, {}) + GM_Globals[GM_CURRENT_API_SERVICES][api][version] = service._rootDesc.copy() + if GM_Globals[GM_CACHE_DISCOVERY_ONLY]: + http.cache = None + return service + except (KeyError, ValueError): + controlflow.invalid_json_exit(disc_file) + +def buildGAPIObject(api): + GM_Globals[GM_CURRENT_API_USER] = None + credentials = getValidOauth2TxtCredentials() + credentials.user_agent = GAM_INFO + http = transport.AuthorizedHttp(credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR])) + service = getService(api, http) + if GC_Values[GC_DOMAIN]: + if not GC_Values[GC_CUSTOMER_ID]: + resp, result = service._http.request(f'https://www.googleapis.com/admin/directory/v1/users?domain={GC_Values[GC_DOMAIN]}&maxResults=1&fields=users(customerId)') + try: + resultObj = json.loads(result) + except ValueError: + controlflow.system_error_exit(8, f'Unexpected response: {result}') + if resp['status'] in ['403', '404']: + try: + message = resultObj['error']['errors'][0]['message'] + except KeyError: + message = resultObj['error']['message'] + controlflow.system_error_exit(8, f'{message} - {GC_Values[GC_DOMAIN]}') + try: + GC_Values[GC_CUSTOMER_ID] = resultObj['users'][0]['customerId'] + except KeyError: + GC_Values[GC_CUSTOMER_ID] = MY_CUSTOMER + else: + GC_Values[GC_DOMAIN] = _getValueFromOAuth('hd', credentials=credentials) + if not GC_Values[GC_CUSTOMER_ID]: + GC_Values[GC_CUSTOMER_ID] = MY_CUSTOMER + return service + +# Convert UID to email address +def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, email_types=['user']): + if isinstance(email_types, str): + email_types = email_types.split(',') + normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID) + if normalizedEmailAddressOrUID.find('@') > 0: + return normalizedEmailAddressOrUID + if not cd: + cd = buildGAPIObject('directory') + if 'user' in email_types: + try: + result = gapi.call(cd.users(), 'get', + throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND], + userKey=normalizedEmailAddressOrUID, fields='primaryEmail') + if 'primaryEmail' in result: + return result['primaryEmail'].lower() + except gapi_errors.GapiUserNotFoundError: + pass + if 'group' in email_types: + try: + result = gapi.call(cd.groups(), 'get', + throw_reasons=[gapi_errors.ErrorReason.GROUP_NOT_FOUND], + groupKey=normalizedEmailAddressOrUID, fields='email') + if 'email' in result: + return result['email'].lower() + except gapi_errors.GapiGroupNotFoundError: + pass + if 'resource' in email_types: + try: + result = gapi.call(cd.resources().calendars(), 'get', + throw_reasons=[gapi_errors.ErrorReason.RESOURCE_NOT_FOUND], + calendarResourceId=normalizedEmailAddressOrUID, + customer=GC_Values[GC_CUSTOMER_ID], fields='resourceEmail') + if 'resourceEmail' in result: + return result['resourceEmail'].lower() + except gapi_errors.GapiResourceNotFoundError: + pass + return normalizedEmailAddressOrUID + +# Convert email address to UID +def convertEmailAddressToUID(emailAddressOrUID, cd=None, email_type='user'): + normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID) + if normalizedEmailAddressOrUID.find('@') > 0: + if not cd: + cd = buildGAPIObject('directory') + if email_type != 'group': + try: + result = gapi.call(cd.users(), 'get', + throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND], + userKey=normalizedEmailAddressOrUID, fields='id') + if 'id' in result: + return result['id'] + except gapi_errors.GapiUserNotFoundError: + pass + try: + result = gapi.call(cd.groups(), 'get', + throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND], + groupKey=normalizedEmailAddressOrUID, fields='id') + if 'id' in result: + return result['id'] + except gapi_errors.GapiNotFoundError: + pass + return None + return normalizedEmailAddressOrUID + +def buildGAPIServiceObject(api, act_as, showAuthError=True): + http = transport.create_http(cache=GM_Globals[GM_CACHE_DIR]) + service = getService(api, http) + GM_Globals[GM_CURRENT_API_USER] = act_as + GM_Globals[GM_CURRENT_API_SCOPES] = API_SCOPE_MAPPING.get(api, service._rootDesc['auth']['oauth2']['scopes']) + credentials = getSvcAcctCredentials(GM_Globals[GM_CURRENT_API_SCOPES], act_as) + request = transport.create_request(http) + retries = 3 + for n in range(1, retries+1): + try: + credentials.refresh(request) + service._http = transport.AuthorizedHttp(credentials, http=http) + break + except (httplib2.ServerNotFoundError, RuntimeError) as e: + if n != retries: + http.connections = {} + controlflow.wait_on_failure(n, retries, str(e)) + continue + controlflow.system_error_exit(4, e) + except google.auth.exceptions.RefreshError as e: + if isinstance(e.args, tuple): + e = e.args[0] + if showAuthError: + display.print_error(f'User {GM_Globals[GM_CURRENT_API_USER]}: {str(e)}') + return gapi.handle_oauth_token_error(str(e), True) + return service + +def buildAlertCenterGAPIObject(user): + userEmail = convertUIDtoEmailAddress(user) + return (userEmail, buildGAPIServiceObject('alertcenter', userEmail)) + +def buildActivityGAPIObject(user): + userEmail = convertUIDtoEmailAddress(user) + return (userEmail, buildGAPIServiceObject('appsactivity', userEmail)) + +def buildDriveGAPIObject(user): + userEmail = convertUIDtoEmailAddress(user) + return (userEmail, buildGAPIServiceObject('drive', userEmail)) + +def buildDrive3GAPIObject(user): + userEmail = convertUIDtoEmailAddress(user) + return (userEmail, buildGAPIServiceObject('drive3', userEmail)) + +def buildGmailGAPIObject(user): + userEmail = convertUIDtoEmailAddress(user) + return (userEmail, buildGAPIServiceObject('gmail', userEmail)) + +def printPassFail(description, result): + print(f' {description:74} {result}') + +def doCheckServiceAccount(users): + i = 5 + test_pass = createGreenText('PASS') + test_fail = createRedText('FAIL') + test_warn = createYellowText('WARN') + check_scopes = [] + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['scope', 'scopes']: + check_scopes = sys.argv[i+1].replace(',', ' ').split() + i += 2 + else: + controlflow.invalid_argument_exit(myarg, "gam user check serviceaccount") + print('Computer clock status:') + timeOffset, nicetime = getLocalGoogleTimeOffset() + if timeOffset < MAX_LOCAL_GOOGLE_TIME_OFFSET: + time_status = test_pass + else: + time_status = test_fail + printPassFail(MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY % ('www.googleapis.com', nicetime), time_status) + oa2 = getService('oauth2', transport.create_http()) + print('Service Account Private Key Authentication:') + # We are explicitly not doing DwD here, just confirming service account can auth + auth_error = '' + try: + credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE], None) + request = transport.create_request() + credentials.refresh(request) + sa_token_info = gapi.call(oa2, 'tokeninfo', access_token=credentials.token) + if sa_token_info: + sa_token_result = test_pass + else: + sa_token_result = test_fail + except google.auth.exceptions.RefreshError as e: + sa_token_result = test_fail + auth_error = str(e.args[0]) + printPassFail(f'Authenticating...{auth_error}', sa_token_result) + if sa_token_result == test_fail: + controlflow.system_error_exit(3, 'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"') + print('Checking key age. Google recommends rotating keys on a routine basis...') + try: + iam = buildGAPIServiceObject('iam', None) + project = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] + key_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] + name = f'projects/-/serviceAccounts/{project}/keys/{key_id}' + key = gapi.call(iam.projects().serviceAccounts().keys(), 'get', name=name, throw_reasons=[gapi_errors.ErrorReason.FOUR_O_THREE]) + # Both Google and GAM set key valid after to day before creation + key_created = dateutil.parser.parse(key['validAfterTime'], ignoretz=True) + datetime.timedelta(days=1) + key_age = datetime.datetime.now() - key_created + key_days = key_age.days + if key_days > 30: + print('Your key is old. Recommend running "gam rotate sakey" to get a new key') + key_age_result = test_warn + else: + key_age_result = test_pass + except googleapiclient.errors.HttpError: + key_age_result = test_warn + key_days = 'UNKNOWN' + print('Unable to check key age, please run "gam update project"') + printPassFail(f'Key is {key_days} days old', key_age_result) + if not check_scopes: + for _, scopes in list(API_SCOPE_MAPPING.items()): + for scope in scopes: + if scope not in check_scopes: + check_scopes.append(scope) + check_scopes.sort() + for user in users: + user = user.lower() + all_scopes_pass = True + #oa2 = getService('oauth2', transport.create_http()) + print(f'Domain-Wide Delegation authentication as {user}:') + for scope in check_scopes: + # try with and without email scope + for scopes in [[scope, USERINFO_EMAIL_SCOPE], [scope]]: + try: + credentials = getSvcAcctCredentials(scopes, user) + credentials.refresh(request) + break + except (httplib2.ServerNotFoundError, RuntimeError) as e: + controlflow.system_error_exit(4, e) + except google.auth.exceptions.RefreshError: + continue + if credentials.token: + token_info = gapi.call(oa2, 'tokeninfo', access_token=credentials.token) + if scope in token_info.get('scope', '').split(' ') and \ + user == token_info.get('email', user).lower(): + result = test_pass + else: + result = test_fail + all_scopes_pass = False + else: + result = test_fail + all_scopes_pass = False + printPassFail(scope, result) + service_account = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] + if all_scopes_pass: + print(f'\nAll scopes passed!\nService account {service_account} is fully authorized.') + continue + user_domain = user[user.find('@')+1:] + # Tack on email scope for more accurate checking + check_scopes.append(USERINFO_EMAIL_SCOPE) + long_url = (f'https://admin.google.com/{user_domain}/ManageOauthClients' + f'?clientScopeToAdd={",".join(check_scopes)}' + f'&clientNameToAdd={service_account}') + short_url = utils.shorten_url(long_url) + scopes_failed = f'''Some scopes failed! To authorize them, please go to: + + {short_url} + +You will be redirected to the G Suite admin console. The Client Name and API +Scopes fields will be pre-populated. Please click Authorize to allow these +scopes access. After authorizing it may take some time for this test to pass so +go grab a cup of coffee and then try this command again. +''' + controlflow.system_error_exit(1, scopes_failed) + +# Batch processing request_id fields +RI_ENTITY = 0 +RI_J = 1 +RI_JCOUNT = 2 +RI_ITEM = 3 +RI_ROLE = 4 + +def batchRequestID(entityName, j, jcount, item, role=''): + return f'{entityName}\n{j}\n{jcount}\n{item}\n{role}' + +def watchGmail(users): + project = f'projects/{_getCurrentProjectID()}' + gamTopics = project+'/topics/gam-pubsub-gmail-' + gamSubscriptions = project+'/subscriptions/gam-pubsub-gmail-' + pubsub = buildGAPIObject('pubsub') + topics = gapi.get_all_pages(pubsub.projects().topics(), 'list', items='topics', project=project) + for atopic in topics: + if atopic['name'].startswith(gamTopics): + topic = atopic['name'] + break + else: + topic = gamTopics+str(uuid.uuid4()) + gapi.call(pubsub.projects().topics(), 'create', name=topic) + body = {'policy': {'bindings': [{'members': ['serviceAccount:gmail-api-push@system.gserviceaccount.com'], 'role': 'roles/pubsub.editor'}]}} + gapi.call(pubsub.projects().topics(), 'setIamPolicy', resource=topic, body=body) + subscriptions = gapi.get_all_pages(pubsub.projects().topics().subscriptions(), 'list', items='subscriptions', topic=topic) + for asubscription in subscriptions: + if asubscription.startswith(gamSubscriptions): + subscription = asubscription + break + else: + subscription = gamSubscriptions+str(uuid.uuid4()) + gapi.call(pubsub.projects().subscriptions(), 'create', name=subscription, body={'topic': topic}) + gmails = {} + for user in users: + gmails[user] = {'g': buildGmailGAPIObject(user)[1]} + gapi.call(gmails[user]['g'].users(), 'watch', userId='me', body={'topicName': topic}) + gmails[user]['seen_historyId'] = gapi.call(gmails[user]['g'].users(), 'getProfile', userId='me', fields='historyId')['historyId'] + print('Watching for events...') + while True: + results = gapi.call(pubsub.projects().subscriptions(), 'pull', subscription=subscription, body={'maxMessages': 100}) + if 'receivedMessages' in results: + ackIds = [] + update_history = [] + for message in results['receivedMessages']: + if 'data' in message['message']: + decoded_message = json.loads(base64.b64decode(message['message']['data'])) + if 'historyId' in decoded_message: + update_history.append(decoded_message['emailAddress']) + if 'ackId' in message: + ackIds.append(message['ackId']) + if ackIds: + gapi.call(pubsub.projects().subscriptions(), 'acknowledge', subscription=subscription, body={'ackIds': ackIds}) + if update_history: + for a_user in update_history: + results = gapi.call(gmails[a_user]['g'].users().history(), 'list', userId='me', startHistoryId=gmails[a_user]['seen_historyId']) + if 'history' in results: + for history in results['history']: + if list(history) == ['messages', 'id']: + continue + if 'labelsAdded' in history: + for labelling in history['labelsAdded']: + print(f'{a_user} labels {", ".join(labelling["labelIds"])} added to {labelling["message"]["id"]}') + if 'labelsRemoved' in history: + for labelling in history['labelsRemoved']: + print(f'{a_user} labels {", ".join(labelling["labelIds"])} removed from {labelling["message"]["id"]}') + if 'messagesDeleted' in history: + for deleting in history['messagesDeleted']: + print(f'{a_user} permanently deleted message {deleting["message"]["id"]}') + if 'messagesAdded' in history: + for adding in history['messagesAdded']: + print(f'{a_user} created message {adding["message"]["id"]} with labels {", ".join(adding["message"]["labelIds"])}') + gmails[a_user]['seen_historyId'] = results['historyId'] + +def addDelegates(users, i): + if i == 4: + if sys.argv[i].lower() != 'to': + controlflow.missing_argument_exit("to", "gam delegate") + i += 1 + delegate = normalizeEmailAddressOrUID(sys.argv[i], noUid=True) + i = 0 + count = len(users) + for delegator in users: + i += 1 + delegator, gmail = buildGmailGAPIObject(delegator) + if not gmail: + continue + print(f'Giving {delegate} delegate access to {delegator}{currentCount(i, count)}') + gapi.call(gmail.users().settings().delegates(), 'create', soft_errors=True, userId='me', body={'delegateEmail': delegate}) + +def gen_sha512_hash(password): + if platform.system() == 'Windows': + return sha512_crypt.hash(password, rounds=5000) + return crypt(password) + +def printShowDelegates(users, csvFormat): + if csvFormat: + todrive = False + csvRows = [] + titles = ['User', 'delegateAddress', 'delegationStatus'] + else: + csvStyle = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if not csvFormat and myarg == 'csv': + csvStyle = True + i += 1 + elif csvFormat and myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show delegates") + count = len(users) + i = 1 + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + sys.stderr.write(f'Getting delegates for {user}{currentCountNL(i, count)}') + i += 1 + delegates = gapi.call(gmail.users().settings().delegates(), 'list', soft_errors=True, userId='me') + if delegates and 'delegates' in delegates: + for delegate in delegates['delegates']: + delegateAddress = delegate['delegateEmail'] + status = delegate['verificationStatus'] + if csvFormat: + row = {'User': user, 'delegateAddress': delegateAddress, 'delegationStatus': status} + csvRows.append(row) + else: + if csvStyle: + print(f'{user},{delegateAddress},{status}') + else: + print(f'Delegator: {user}\n Status: {status}\n Delegate Email: {delegateAddress}\n') + if not csvFormat and not csvStyle and delegates['delegates']: + print(f'Total {len(delegates["delegates"])}') + if csvFormat: + display.write_csv_file(csvRows, titles, 'Delegates', todrive) + +def deleteDelegate(users): + delegate = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Deleting {delegate} delegate access to {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings().delegates(), 'delete', soft_errors=True, userId='me', delegateEmail=delegate) + +def doAddCourseParticipant(): + croom = buildGAPIObject('classroom') + courseId = addCourseIdScope(sys.argv[2]) + noScopeCourseId = removeCourseIdScope(courseId) + participant_type = sys.argv[4].lower() + new_id = sys.argv[5] + if participant_type in ['student', 'students']: + new_id = normalizeEmailAddressOrUID(new_id) + gapi.call(croom.courses().students(), 'create', courseId=courseId, body={'userId': new_id}) + print(f'Added {new_id} as a student of course {noScopeCourseId}') + elif participant_type in ['teacher', 'teachers']: + new_id = normalizeEmailAddressOrUID(new_id) + gapi.call(croom.courses().teachers(), 'create', courseId=courseId, body={'userId': new_id}) + print(f'Added {new_id} as a teacher of course {noScopeCourseId}') + elif participant_type in ['alias']: + new_id = addCourseIdScope(new_id) + gapi.call(croom.courses().aliases(), 'create', courseId=courseId, body={'alias': new_id}) + print(f'Added {removeCourseIdScope(new_id)} as an alias of course {noScopeCourseId}') + else: + controlflow.invalid_argument_exit(participant_type, "gam course ID add") + +def doSyncCourseParticipants(): + courseId = addCourseIdScope(sys.argv[2]) + participant_type = sys.argv[4].lower() + diff_entity_type = sys.argv[5].lower() + diff_entity = sys.argv[6] + current_course_users = getUsersToModify(entity_type=participant_type, entity=courseId) + print() + current_course_users = [x.lower() for x in current_course_users] + if diff_entity_type == 'courseparticipants': + diff_entity_type = participant_type + diff_against_users = getUsersToModify(entity_type=diff_entity_type, entity=diff_entity) + print() + diff_against_users = [x.lower() for x in diff_against_users] + to_add = list(set(diff_against_users) - set(current_course_users)) + to_remove = list(set(current_course_users) - set(diff_against_users)) + gam_commands = [] + for add_email in to_add: + gam_commands.append(['gam', 'course', courseId, 'add', participant_type, add_email]) + for remove_email in to_remove: + gam_commands.append(['gam', 'course', courseId, 'remove', participant_type, remove_email]) + run_batch(gam_commands) + +def doDelCourseParticipant(): + croom = buildGAPIObject('classroom') + courseId = addCourseIdScope(sys.argv[2]) + noScopeCourseId = removeCourseIdScope(courseId) + participant_type = sys.argv[4].lower() + remove_id = sys.argv[5] + if participant_type in ['student', 'students']: + remove_id = normalizeEmailAddressOrUID(remove_id) + gapi.call(croom.courses().students(), 'delete', courseId=courseId, userId=remove_id) + print(f'Removed {remove_id} as a student of course {noScopeCourseId}') + elif participant_type in ['teacher', 'teachers']: + remove_id = normalizeEmailAddressOrUID(remove_id) + gapi.call(croom.courses().teachers(), 'delete', courseId=courseId, userId=remove_id) + print(f'Removed {remove_id} as a teacher of course {noScopeCourseId}') + elif participant_type in ['alias']: + remove_id = addCourseIdScope(remove_id) + gapi.call(croom.courses().aliases(), 'delete', courseId=courseId, alias=remove_id) + print(f'Removed {removeCourseIdScope(remove_id)} as an alias of course {noScopeCourseId}') + else: + controlflow.invalid_argument_exit(participant_type, "gam course ID delete") + +def doDelCourse(): + croom = buildGAPIObject('classroom') + courseId = addCourseIdScope(sys.argv[3]) + gapi.call(croom.courses(), 'delete', id=courseId) + print(f'Deleted Course {courseId}') + +def _getValidatedState(state, validStates): + state = state.upper() + if state not in validStates: + controlflow.expected_argument_exit("course state", ", ".join(validStates).lower(), state.lower()) + return state + +def getCourseAttribute(myarg, value, body, croom, function): + if myarg == 'name': + body['name'] = value + elif myarg == 'section': + body['section'] = value + elif myarg == 'heading': + body['descriptionHeading'] = value + elif myarg == 'description': + body['description'] = value.replace('\\n', '\n') + elif myarg == 'room': + body['room'] = value + elif myarg in ['owner', 'ownerid', 'teacher']: + body['ownerId'] = normalizeEmailAddressOrUID(value) + elif myarg in ['state', 'status']: + validStates = gapi.get_enum_values_minus_unspecified(croom._rootDesc['schemas']['Course']['properties']['courseState']['enum']) + body['courseState'] = _getValidatedState(value, validStates) + else: + controlflow.invalid_argument_exit(myarg, f"gam {function} course") + +def _getCourseStates(croom, value, courseStates): + validStates = gapi.get_enum_values_minus_unspecified(croom._rootDesc['schemas']['Course']['properties']['courseState']['enum']) + for state in value.replace(',', ' ').split(): + courseStates.append(_getValidatedState(state, validStates)) + +def doUpdateCourse(): + croom = buildGAPIObject('classroom') + courseId = addCourseIdScope(sys.argv[3]) + body = {} + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + getCourseAttribute(myarg, sys.argv[i+1], body, croom, 'update') + i += 2 + updateMask = ','.join(list(body)) + body['id'] = courseId + result = gapi.call(croom.courses(), 'patch', id=courseId, body=body, updateMask=updateMask) + print(f'Updated Course {result["id"]}') + +def doCreateDomain(): + cd = buildGAPIObject('directory') + domain_name = sys.argv[3] + body = {'domainName': domain_name} + gapi.call(cd.domains(), 'insert', customer=GC_Values[GC_CUSTOMER_ID], body=body) + print(f'Added domain {domain_name}') + +def doCreateDomainAlias(): + cd = buildGAPIObject('directory') + body = {} + body['domainAliasName'] = sys.argv[3] + body['parentDomainName'] = sys.argv[4] + gapi.call(cd.domainAliases(), 'insert', customer=GC_Values[GC_CUSTOMER_ID], body=body) + +def doUpdateDomain(): + cd = buildGAPIObject('directory') + domain_name = sys.argv[3] + i = 4 + body = {} + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'primary': + body['customerDomain'] = domain_name + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam update domain") + gapi.call(cd.customers(), 'update', customerKey=GC_Values[GC_CUSTOMER_ID], body=body) + print(f'{domain_name} is now the primary domain.') + +def doGetDomainInfo(): + if (len(sys.argv) < 4) or (sys.argv[3] == 'logo'): + gapi_directory_customer.doGetCustomerInfo() + return + cd = buildGAPIObject('directory') + domainName = sys.argv[3] + result = gapi.call(cd.domains(), 'get', customer=GC_Values[GC_CUSTOMER_ID], domainName=domainName) + if 'creationTime' in result: + result['creationTime'] = utils.formatTimestampYMDHMSF(result['creationTime']) + if 'domainAliases' in result: + for i in range(0, len(result['domainAliases'])): + if 'creationTime' in result['domainAliases'][i]: + result['domainAliases'][i]['creationTime'] = utils.formatTimestampYMDHMSF(result['domainAliases'][i]['creationTime']) + display.print_json(result) + +def doGetDomainAliasInfo(): + cd = buildGAPIObject('directory') + alias = sys.argv[3] + result = gapi.call(cd.domainAliases(), 'get', customer=GC_Values[GC_CUSTOMER_ID], domainAliasName=alias) + if 'creationTime' in result: + result['creationTime'] = utils.formatTimestampYMDHMSF(result['creationTime']) + display.print_json(result) + +def doDelDomain(): + cd = buildGAPIObject('directory') + domainName = sys.argv[3] + gapi.call(cd.domains(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], domainName=domainName) + +def doDelDomainAlias(): + cd = buildGAPIObject('directory') + domainAliasName = sys.argv[3] + gapi.call(cd.domainAliases(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], domainAliasName=domainAliasName) + +def doPrintDomains(): + cd = buildGAPIObject('directory') + todrive = False + titles = ['domainName',] + csvRows = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print domains") + results = gapi.call(cd.domains(), 'list', customer=GC_Values[GC_CUSTOMER_ID]) + for domain in results['domains']: + domain_attributes = {} + domain['type'] = ['secondary', 'primary'][domain['isPrimary']] + for attr in domain: + if attr in ['kind', 'etag', 'domainAliases', 'isPrimary']: + continue + if attr in ['creationTime',]: + domain[attr] = utils.formatTimestampYMDHMSF(domain[attr]) + if attr not in titles: + titles.append(attr) + domain_attributes[attr] = domain[attr] + csvRows.append(domain_attributes) + if 'domainAliases' in domain: + for aliasdomain in domain['domainAliases']: + aliasdomain['domainName'] = aliasdomain['domainAliasName'] + del aliasdomain['domainAliasName'] + aliasdomain['type'] = 'alias' + aliasdomain_attributes = {} + for attr in aliasdomain: + if attr in ['kind', 'etag']: + continue + if attr in ['creationTime',]: + aliasdomain[attr] = utils.formatTimestampYMDHMSF(aliasdomain[attr]) + if attr not in titles: + titles.append(attr) + aliasdomain_attributes[attr] = aliasdomain[attr] + csvRows.append(aliasdomain_attributes) + display.write_csv_file(csvRows, titles, 'Domains', todrive) + +def doPrintDomainAliases(): + cd = buildGAPIObject('directory') + todrive = False + titles = ['domainAliasName',] + csvRows = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print domainaliases") + results = gapi.call(cd.domainAliases(), 'list', customer=GC_Values[GC_CUSTOMER_ID]) + for domainAlias in results['domainAliases']: + domainAlias_attributes = {} + for attr in domainAlias: + if attr in ['kind', 'etag']: + continue + if attr == 'creationTime': + domainAlias[attr] = utils.formatTimestampYMDHMSF(domainAlias[attr]) + if attr not in titles: + titles.append(attr) + domainAlias_attributes[attr] = domainAlias[attr] + csvRows.append(domainAlias_attributes) + display.write_csv_file(csvRows, titles, 'Domains', todrive) + +def doDelAdmin(): + cd = buildGAPIObject('directory') + roleAssignmentId = sys.argv[3] + print(f'Deleting Admin Role Assignment {roleAssignmentId}') + gapi.call(cd.roleAssignments(), 'delete', + customer=GC_Values[GC_CUSTOMER_ID], roleAssignmentId=roleAssignmentId) + +def doCreateAdmin(): + cd = buildGAPIObject('directory') + user = normalizeEmailAddressOrUID(sys.argv[3]) + body = {'assignedTo': convertEmailAddressToUID(user, cd)} + role = sys.argv[4] + body['roleId'] = getRoleId(role) + body['scopeType'] = sys.argv[5].upper() + if body['scopeType'] not in ['CUSTOMER', 'ORG_UNIT']: + controlflow.expected_argument_exit("scope type", ", ".join(["customer", "org_unit"]), body["scopeType"]) + if body['scopeType'] == 'ORG_UNIT': + orgUnit, orgUnitId = getOrgUnitId(sys.argv[6], cd) + body['orgUnitId'] = orgUnitId[3:] + scope = f'ORG_UNIT {orgUnit}' + else: + scope = 'CUSTOMER' + print(f'Giving {user} admin role {role} for {scope}') + gapi.call(cd.roleAssignments(), 'insert', + customer=GC_Values[GC_CUSTOMER_ID], body=body) + +def doPrintAdminRoles(): + cd = buildGAPIObject('directory') + todrive = False + titles = ['roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', 'isSystemRole'] + fields = f'nextPageToken,items({",".join(titles)})' + csvRows = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print adminroles") + roles = gapi.get_all_pages(cd.roles(), 'list', 'items', + customer=GC_Values[GC_CUSTOMER_ID], fields=fields) + for role in roles: + role_attrib = {} + for key, value in list(role.items()): + role_attrib[key] = value + csvRows.append(role_attrib) + display.write_csv_file(csvRows, titles, 'Admin Roles', todrive) + +def doPrintAdmins(): + cd = buildGAPIObject('directory') + roleId = None + userKey = None + todrive = False + fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)' + titles = ['roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser', 'scopeType', 'orgUnitId', 'orgUnit'] + csvRows = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'user': + userKey = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg == 'role': + roleId = getRoleId(sys.argv[i+1]) + i += 2 + elif myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print admins") + admins = gapi.get_all_pages(cd.roleAssignments(), 'list', 'items', + customer=GC_Values[GC_CUSTOMER_ID], userKey=userKey, roleId=roleId, fields=fields) + for admin in admins: + admin_attrib = {} + for key, value in list(admin.items()): + if key == 'assignedTo': + admin_attrib['assignedToUser'] = user_from_userid(value) + elif key == 'roleId': + admin_attrib['role'] = role_from_roleid(value) + elif key == 'orgUnitId': + value = f'id:{value}' + admin_attrib['orgUnit'] = orgunit_from_orgunitid(value) + admin_attrib[key] = value + csvRows.append(admin_attrib) + display.write_csv_file(csvRows, titles, 'Admins', todrive) + +def buildOrgUnitIdToNameMap(): + cd = buildGAPIObject('directory') + result = gapi.call(cd.orgunits(), 'list', + customerId=GC_Values[GC_CUSTOMER_ID], + fields='organizationUnits(orgUnitPath,orgUnitId)', type='all') + GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME] = {} + for orgUnit in result['organizationUnits']: + GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME][orgUnit['orgUnitId']] = orgUnit['orgUnitPath'] + +def orgunit_from_orgunitid(orgunitid): + if not GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME]: + buildOrgUnitIdToNameMap() + return GM_Globals[GM_MAP_ORGUNIT_ID_TO_NAME].get(orgunitid, orgunitid) + +def buildRoleIdToNameToIdMap(): + cd = buildGAPIObject('directory') + result = gapi.get_all_pages(cd.roles(), 'list', 'items', + customer=GC_Values[GC_CUSTOMER_ID], + fields='nextPageToken,items(roleId,roleName)') + GM_Globals[GM_MAP_ROLE_ID_TO_NAME] = {} + GM_Globals[GM_MAP_ROLE_NAME_TO_ID] = {} + for role in result: + GM_Globals[GM_MAP_ROLE_ID_TO_NAME][role['roleId']] = role['roleName'] + GM_Globals[GM_MAP_ROLE_NAME_TO_ID][role['roleName']] = role['roleId'] + +def role_from_roleid(roleid): + if not GM_Globals[GM_MAP_ROLE_ID_TO_NAME]: + buildRoleIdToNameToIdMap() + return GM_Globals[GM_MAP_ROLE_ID_TO_NAME].get(roleid, roleid) + +def roleid_from_role(role): + if not GM_Globals[GM_MAP_ROLE_NAME_TO_ID]: + buildRoleIdToNameToIdMap() + return GM_Globals[GM_MAP_ROLE_NAME_TO_ID].get(role, None) + +def getRoleId(role): + cg = UID_PATTERN.match(role) + if cg: + roleId = cg.group(1) + else: + roleId = roleid_from_role(role) + if not roleId: + controlflow.system_error_exit(4, f'{role} is not a valid role. Please ensure role name is exactly as shown in admin console.') + return roleId + +def buildUserIdToNameMap(): + cd = buildGAPIObject('directory') + result = gapi.get_all_pages(cd.users(), 'list', 'users', + customer=GC_Values[GC_CUSTOMER_ID], + fields='nextPageToken,users(id,primaryEmail)') + GM_Globals[GM_MAP_USER_ID_TO_NAME] = {} + for user in result: + GM_Globals[GM_MAP_USER_ID_TO_NAME][user['id']] = user['primaryEmail'] + +def user_from_userid(userid): + if not GM_Globals[GM_MAP_USER_ID_TO_NAME]: + buildUserIdToNameMap() + return GM_Globals[GM_MAP_USER_ID_TO_NAME].get(userid, '') + +def appID2app(dt, appID): + for serviceName, serviceID in list(SERVICE_NAME_TO_ID_MAP.items()): + if appID == serviceID: + return serviceName + online_services = gapi.get_all_pages(dt.applications(), 'list', 'applications', customerId=GC_Values[GC_CUSTOMER_ID]) + for online_service in online_services: + if appID == online_service['id']: + return online_service['name'] + return f'applicationId: {appID}' + +def app2appID(dt, app): + serviceName = app.lower() + if serviceName in SERVICE_NAME_CHOICES_MAP: + return (SERVICE_NAME_CHOICES_MAP[serviceName], SERVICE_NAME_TO_ID_MAP[SERVICE_NAME_CHOICES_MAP[serviceName]]) + online_services = gapi.get_all_pages(dt.applications(), 'list', 'applications', customerId=GC_Values[GC_CUSTOMER_ID]) + for online_service in online_services: + if serviceName == online_service['name'].lower(): + return (online_service['name'], online_service['id']) + controlflow.system_error_exit(2, f'{app} is not a valid service for data transfer.') + +def convertToUserID(user): + cg = UID_PATTERN.match(user) + if cg: + return cg.group(1) + cd = buildGAPIObject('directory') + if user.find('@') == -1: + user = f'{user}@{GC_Values[GC_DOMAIN]}' + try: + return gapi.call(cd.users(), 'get', throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND, gapi_errors.ErrorReason.BAD_REQUEST, gapi_errors.ErrorReason.FORBIDDEN], userKey=user, fields='id')['id'] + except (gapi_errors.GapiUserNotFoundError, gapi_errors.GapiBadRequestError, gapi_errors.GapiForbiddenError): + controlflow.system_error_exit(3, f'no such user {user}') + +def convertUserIDtoEmail(uid): + cd = buildGAPIObject('directory') + try: + return gapi.call(cd.users(), 'get', throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND, gapi_errors.ErrorReason.BAD_REQUEST, gapi_errors.ErrorReason.FORBIDDEN], userKey=uid, fields='primaryEmail')['primaryEmail'] + except (gapi_errors.GapiUserNotFoundError, gapi_errors.GapiBadRequestError, gapi_errors.GapiForbiddenError): + return f'uid:{uid}' + +def doCreateDataTransfer(): + dt = buildGAPIObject('datatransfer') + body = {} + old_owner = sys.argv[3] + body['oldOwnerUserId'] = convertToUserID(old_owner) + apps = sys.argv[4].split(",") + appNameList = [] + appIDList = [] + i = 0 + while i < len(apps): + serviceName, serviceID = app2appID(dt, apps[i]) + appNameList.append(serviceName) + appIDList.append({'applicationId': serviceID}) + i += 1 + body['applicationDataTransfers'] = (appIDList) + new_owner = sys.argv[5] + body['newOwnerUserId'] = convertToUserID(new_owner) + parameters = {} + i = 6 + while i < len(sys.argv): + parameters[sys.argv[i].upper()] = sys.argv[i+1].upper().split(',') + i += 2 + i = 0 + for key, value in list(parameters.items()): + body['applicationDataTransfers'][i].setdefault('applicationTransferParams', []) + body['applicationDataTransfers'][i]['applicationTransferParams'].append({'key': key, 'value': value}) + i += 1 + result = gapi.call(dt.transfers(), 'insert', body=body, fields='id')['id'] + print(f'Submitted request id {result} to transfer {",".join(map(str, appNameList))} from {old_owner} to {new_owner}') + +def doPrintTransferApps(): + dt = buildGAPIObject('datatransfer') + apps = gapi.get_all_pages(dt.applications(), 'list', 'applications', customerId=GC_Values[GC_CUSTOMER_ID]) + display.print_json(apps) + +def doPrintDataTransfers(): + dt = buildGAPIObject('datatransfer') + i = 3 + newOwnerUserId = None + oldOwnerUserId = None + status = None + todrive = False + titles = ['id',] + csvRows = [] + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg in ['olduser', 'oldowner']: + oldOwnerUserId = convertToUserID(sys.argv[i+1]) + i += 2 + elif myarg in ['newuser', 'newowner']: + newOwnerUserId = convertToUserID(sys.argv[i+1]) + i += 2 + elif myarg == 'status': + status = sys.argv[i+1] + i += 2 + elif myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print transfers") + transfers = gapi.get_all_pages(dt.transfers(), 'list', 'dataTransfers', + customerId=GC_Values[GC_CUSTOMER_ID], status=status, + newOwnerUserId=newOwnerUserId, oldOwnerUserId=oldOwnerUserId) + for transfer in transfers: + for i in range(0, len(transfer['applicationDataTransfers'])): + a_transfer = {} + a_transfer['oldOwnerUserEmail'] = convertUserIDtoEmail(transfer['oldOwnerUserId']) + a_transfer['newOwnerUserEmail'] = convertUserIDtoEmail(transfer['newOwnerUserId']) + a_transfer['requestTime'] = transfer['requestTime'] + a_transfer['applicationId'] = transfer['applicationDataTransfers'][i]['applicationId'] + a_transfer['application'] = appID2app(dt, a_transfer['applicationId']) + a_transfer['status'] = transfer['applicationDataTransfers'][i]['applicationTransferStatus'] + a_transfer['id'] = transfer['id'] + if 'applicationTransferParams' in transfer['applicationDataTransfers'][i]: + for param in transfer['applicationDataTransfers'][i]['applicationTransferParams']: + a_transfer[param['key']] = ','.join(param.get('value', [])) + for title in a_transfer: + if title not in titles: + titles.append(title) + csvRows.append(a_transfer) + display.write_csv_file(csvRows, titles, 'Data Transfers', todrive) + +def doGetDataTransferInfo(): + dt = buildGAPIObject('datatransfer') + dtId = sys.argv[3] + transfer = gapi.call(dt.transfers(), 'get', dataTransferId=dtId) + print(f'Old Owner: {convertUserIDtoEmail(transfer["oldOwnerUserId"])}') + print(f'New Owner: {convertUserIDtoEmail(transfer["newOwnerUserId"])}') + print(f'Request Time: {transfer["requestTime"]}') + for app in transfer['applicationDataTransfers']: + print(f'Application: {appID2app(dt, app["applicationId"])}') + print(f'Status: {app["applicationTransferStatus"]}') + print('Parameters:') + if 'applicationTransferParams' in app: + for param in app['applicationTransferParams']: + print(f' {param["key"]}: {",".join(param.get("value", []))}') + else: + print(' None') + print() + +def doPrintShowGuardians(csvFormat): + croom = buildGAPIObject('classroom') + invitedEmailAddress = None + studentIds = ['-',] + states = None + service = croom.userProfiles().guardians() + items = 'guardians' + itemName = 'Guardians' + if csvFormat: + csvRows = [] + todrive = False + titles = ['studentEmail', 'studentId', 'invitedEmailAddress', 'guardianId'] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'invitedguardian': + invitedEmailAddress = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg == 'student': + studentIds = [normalizeStudentGuardianEmailAddressOrUID(sys.argv[i+1])] + i += 2 + elif myarg == 'invitations': + service = croom.userProfiles().guardianInvitations() + items = 'guardianInvitations' + itemName = 'Guardian Invitations' + titles = ['studentEmail', 'studentId', 'invitedEmailAddress', 'invitationId'] + if states is None: + states = ['COMPLETE', 'PENDING', 'GUARDIAN_INVITATION_STATE_UNSPECIFIED'] + i += 1 + elif myarg == 'states': + states = sys.argv[i+1].upper().replace(',', ' ').split() + i += 2 + elif myarg in usergroup_types: + studentIds = getUsersToModify(entity_type=myarg, entity=sys.argv[i+1]) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {['show', 'print'][csvFormat]} guardians") + i = 0 + count = len(studentIds) + for studentId in studentIds: + i += 1 + studentId = normalizeStudentGuardianEmailAddressOrUID(studentId) + kwargs = {'invitedEmailAddress': invitedEmailAddress, 'studentId': studentId} + if items == 'guardianInvitations': + kwargs['states'] = states + if studentId != '-': + if csvFormat: + sys.stderr.write('\r') + sys.stderr.flush() + sys.stderr.write(f'Getting {itemName} for {studentId}{currentCount(i, count)}{" " * 40}') + guardians = gapi.get_all_pages(service, 'list', items, soft_errors=True, **kwargs) + if not csvFormat: + print(f'Student: {studentId}, {itemName}:{currentCount(i, count)}') + for guardian in guardians: + display.print_json(guardian, spacing=' ') + else: + for guardian in guardians: + guardian['studentEmail'] = studentId + display.add_row_titles_to_csv_file(utils.flatten_json(guardian), csvRows, titles) + if csvFormat: + sys.stderr.write('\n') + display.write_csv_file(csvRows, titles, itemName, todrive) + +def doInviteGuardian(): + croom = buildGAPIObject('classroom') + body = {'invitedEmailAddress': normalizeEmailAddressOrUID(sys.argv[3])} + studentId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[4]) + result = gapi.call(croom.userProfiles().guardianInvitations(), 'create', studentId=studentId, body=body) + print(f'Invited email {result["invitedEmailAddress"]} as guardian of {studentId}. Invite ID {result["invitationId"]}') + +def _cancelGuardianInvitation(croom, studentId, invitationId): + try: + result = gapi.call(croom.userProfiles().guardianInvitations(), 'patch', + throw_reasons=[gapi_errors.ErrorReason.FAILED_PRECONDITION, gapi_errors.ErrorReason.FORBIDDEN, gapi_errors.ErrorReason.NOT_FOUND], + studentId=studentId, invitationId=invitationId, updateMask='state', body={'state': 'COMPLETE'}) + print(f'Cancelled PENDING guardian invitation for {result["invitedEmailAddress"]} as guardian of {studentId}') + return True + except gapi_errors.GapiFailedPreconditionError: + display.print_error(f'Guardian invitation {invitationId} for {studentId} status is not PENDING') + GM_Globals[GM_SYSEXITRC] = 3 + return True + except gapi_errors.GapiForbiddenError: + entityUnknownWarning('Student', studentId, 0, 0) + sys.exit(3) + except gapi_errors.GapiNotFoundError: + return False + +def doCancelGuardianInvitation(): + croom = buildGAPIObject('classroom') + invitationId = sys.argv[3] + studentId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[4]) + if not _cancelGuardianInvitation(croom, studentId, invitationId): + controlflow.system_error_exit(3, f'Guardian invitation {invitationId} for {studentId} does not exist') + +def _deleteGuardian(croom, studentId, guardianId, guardianEmail): + try: + gapi.call(croom.userProfiles().guardians(), 'delete', + throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN, gapi_errors.ErrorReason.NOT_FOUND], + studentId=studentId, guardianId=guardianId) + print(f'Deleted {guardianEmail} as a guardian of {studentId}') + return True + except gapi_errors.GapiForbiddenError: + entityUnknownWarning('Student', studentId, 0, 0) + sys.exit(3) + except gapi_errors.GapiNotFoundError: + return False + +def doDeleteGuardian(): + croom = buildGAPIObject('classroom') + invitationsOnly = False + guardianId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[3]) + guardianIdIsEmail = guardianId.find('@') != -1 + studentId = normalizeStudentGuardianEmailAddressOrUID(sys.argv[4]) + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['invitation', 'invitations']: + invitationsOnly = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam delete guardian") + if not invitationsOnly: + if guardianIdIsEmail: + try: + results = gapi.get_all_pages(croom.userProfiles().guardians(), 'list', 'guardians', + throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN], + studentId=studentId, invitedEmailAddress=guardianId, + fields='nextPageToken,guardians(studentId,guardianId)') + if results: + for result in results: + _deleteGuardian(croom, result['studentId'], result['guardianId'], guardianId) + return + except gapi_errors.GapiForbiddenError: + entityUnknownWarning('Student', studentId, 0, 0) + sys.exit(3) + else: + if _deleteGuardian(croom, studentId, guardianId, guardianId): + return + # See if there's a pending invitation + if guardianIdIsEmail: + try: + results = gapi.get_all_pages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations', + throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN], + studentId=studentId, invitedEmailAddress=guardianId, states=['PENDING',], + fields='nextPageToken,guardianInvitations(studentId,invitationId)') + if results: + for result in results: + status = _cancelGuardianInvitation(croom, result['studentId'], result['invitationId']) + sys.exit(status) + except gapi_errors.GapiForbiddenError: + entityUnknownWarning('Student', studentId, 0, 0) + sys.exit(3) + else: + if _cancelGuardianInvitation(croom, studentId, guardianId): + return + controlflow.system_error_exit(3, f'{guardianId} is not a guardian of {studentId} and no invitation exists.') + +def doCreateCourse(): + croom = buildGAPIObject('classroom') + body = {} + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['alias', 'id']: + body['id'] = f'd:{sys.argv[i+1]}' + i += 2 + else: + getCourseAttribute(myarg, sys.argv[i+1], body, croom, 'create') + i += 2 + if 'ownerId' not in body: + controlflow.system_error_exit(2, 'expected teacher )') + if 'name' not in body: + controlflow.system_error_exit(2, 'expected name )') + result = gapi.call(croom.courses(), 'create', body=body) + print(f'Created course {result["id"]}') + +def doGetCourseInfo(): + croom = buildGAPIObject('classroom') + courseId = addCourseIdScope(sys.argv[3]) + info = gapi.call(croom.courses(), 'get', id=courseId) + info['ownerEmail'] = convertUIDtoEmailAddress(f'uid:{info["ownerId"]}') + display.print_json(info) + teachers = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', courseId=courseId) + students = gapi.get_all_pages(croom.courses().students(), 'list', 'students', courseId=courseId) + try: + aliases = gapi.get_all_pages(croom.courses().aliases(), 'list', 'aliases', throw_reasons=[gapi_errors.ErrorReason.NOT_IMPLEMENTED], courseId=courseId) + except gapi_errors.GapiNotImplementedError: + aliases = [] + if aliases: + print('Aliases:') + for alias in aliases: + print(f' {alias["alias"][2:]}') + print('Participants:') + print(' Teachers:') + for teacher in teachers: + try: + print(f' {teacher["profile"]["name"]["fullName"]} - {teacher["profile"]["emailAddress"]}') + except KeyError: + print(f' {teacher["profile"]["name"]["fullName"]}') + print(' Students:') + for student in students: + try: + print(f' {student["profile"]["name"]["fullName"]} - {student["profile"]["emailAddress"]}') + except KeyError: + print(f' {student["profile"]["name"]["fullName"]}') + +COURSE_ARGUMENT_TO_PROPERTY_MAP = { + 'alternatelink': 'alternateLink', + 'coursegroupemail': 'courseGroupEmail', + 'coursematerialsets': 'courseMaterialSets', + 'coursestate': 'courseState', + 'creationtime': 'creationTime', + 'description': 'description', + 'descriptionheading': 'descriptionHeading', + 'enrollmentcode': 'enrollmentCode', + 'guardiansenabled': 'guardiansEnabled', + 'id': 'id', + 'name': 'name', + 'ownerid': 'ownerId', + 'room': 'room', + 'section': 'section', + 'teacherfolder': 'teacherFolder', + 'teachergroupemail': 'teacherGroupEmail', + 'updatetime': 'updateTime', + } + +def doPrintCourses(): + + def _processFieldsList(myarg, i, fList): + fieldNameList = sys.argv[i+1] + for field in fieldNameList.lower().replace(',', ' ').split(): + if field in COURSE_ARGUMENT_TO_PROPERTY_MAP: + if field != 'id': + fList.append(COURSE_ARGUMENT_TO_PROPERTY_MAP[field]) + else: + controlflow.invalid_argument_exit(field, f"gam print courses {myarg}") + + def _saveParticipants(course, participants, role): + jcount = len(participants) + course[role] = jcount + display.add_titles_to_csv_file([role], titles) + if countsOnly: + return + j = 0 + for member in participants: + memberTitles = [] + prefix = f'{role}.{j}.' + profile = member['profile'] + emailAddress = profile.get('emailAddress') + if emailAddress: + memberTitle = prefix+'emailAddress' + course[memberTitle] = emailAddress + memberTitles.append(memberTitle) + memberId = profile.get('id') + if memberId: + memberTitle = prefix+'id' + course[memberTitle] = memberId + memberTitles.append(memberTitle) + fullName = profile.get('name', {}).get('fullName') + if fullName: + memberTitle = prefix+'name.fullName' + course[memberTitle] = fullName + memberTitles.append(memberTitle) + display.add_titles_to_csv_file(memberTitles, titles) + j += 1 + + croom = buildGAPIObject('classroom') + todrive = False + fieldsList = [] + skipFieldsList = [] + titles = ['id',] + csvRows = [] + ownerEmails = studentId = teacherId = None + courseStates = [] + countsOnly = showAliases = False + delimiter = ' ' + showMembers = '' + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'teacher': + teacherId = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg == 'student': + studentId = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg in ['state', 'states', 'status']: + _getCourseStates(croom, sys.argv[i+1], courseStates) + i += 2 + elif myarg == 'todrive': + todrive = True + i += 1 + elif myarg in ['alias', 'aliases']: + showAliases = True + i += 1 + elif myarg == 'countsonly': + countsOnly = True + i += 1 + elif myarg == 'delimiter': + delimiter = sys.argv[i+1] + i += 2 + elif myarg == 'show': + showMembers = sys.argv[i+1].lower() + validShows = ['all', 'students', 'teachers'] + if showMembers not in validShows: + controlflow.expected_argument_exit("show", ", ".join(validShows), showMembers) + i += 2 + elif myarg == 'fields': + if not fieldsList: + fieldsList = ['id',] + _processFieldsList(myarg, i, fieldsList) + i += 2 + elif myarg == 'skipfields': + _processFieldsList(myarg, i, skipFieldsList) + i += 2 + elif myarg == 'owneremail': + ownerEmails = {} + cd = buildGAPIObject('directory') + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print courses") + if ownerEmails is not None and fieldsList: + fieldsList.append('ownerId') + fields = f'nextPageToken,courses({",".join(set(fieldsList))})' if fieldsList else None + printGettingAllItems('Courses', None) + page_message = gapi.got_total_items_msg('Courses', '...\n') + all_courses = gapi.get_all_pages(croom.courses(), 'list', 'courses', page_message=page_message, teacherId=teacherId, studentId=studentId, courseStates=courseStates, fields=fields) + for course in all_courses: + if ownerEmails is not None: + ownerId = course['ownerId'] + if ownerId not in ownerEmails: + ownerEmails[ownerId] = convertUIDtoEmailAddress(f'uid{ownerId}', cd=cd) + course['ownerEmail'] = ownerEmails[ownerId] + for field in skipFieldsList: + course.pop(field, None) + display.add_row_titles_to_csv_file(utils.flatten_json(course), csvRows, titles) + if showAliases or showMembers: + if showAliases: + titles.append('Aliases') + if showMembers: + if countsOnly: + teachersFields = 'nextPageToken,teachers(profile(id))' + studentsFields = 'nextPageToken,students(profile(id))' + else: + teachersFields = 'nextPageToken,teachers(profile)' + studentsFields = 'nextPageToken,students(profile)' + i = 0 + count = len(csvRows) + for course in csvRows: + i += 1 + courseId = course['id'] + if showAliases: + alias_message = gapi.got_total_items_msg(f'Aliases for course {courseId}{currentCount(i, count)}', '') + course_aliases = gapi.get_all_pages(croom.courses().aliases(), 'list', 'aliases', + page_message=alias_message, + courseId=courseId) + course['Aliases'] = delimiter.join([alias['alias'][2:] for alias in course_aliases]) + if showMembers: + if showMembers != 'students': + teacher_message = gapi.got_total_items_msg(f'Teachers for course {courseId}{currentCount(i, count)}', '') + results = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', + page_message=teacher_message, + courseId=courseId, fields=teachersFields) + _saveParticipants(course, results, 'teachers') + if showMembers != 'teachers': + student_message = gapi.got_total_items_msg(f'Students for course {courseId}{currentCount(i, count)}', '') + results = gapi.get_all_pages(croom.courses().students(), 'list', 'students', + page_message=student_message, + courseId=courseId, fields=studentsFields) + _saveParticipants(course, results, 'students') + display.sort_csv_titles(['id', 'name'], titles) + display.write_csv_file(csvRows, titles, 'Courses', todrive) + +def doPrintCourseParticipants(): + croom = buildGAPIObject('classroom') + todrive = False + titles = ['courseId',] + csvRows = [] + courses = [] + teacherId = None + studentId = None + courseStates = [] + showMembers = 'all' + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['course', 'class']: + courses.append(addCourseIdScope(sys.argv[i+1])) + i += 2 + elif myarg == 'teacher': + teacherId = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg == 'student': + studentId = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg in ['state', 'states', 'status']: + _getCourseStates(croom, sys.argv[i+1], courseStates) + i += 2 + elif myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'show': + showMembers = sys.argv[i+1].lower() + validShows = ['all', 'students', 'teachers'] + if showMembers not in validShows: + controlflow.expected_argument_exit("show", ", ".join(validShows), showMembers) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print course-participants") + if not courses: + printGettingAllItems('Courses', None) + page_message = gapi.got_total_items_msg('Courses', '...\n') + all_courses = gapi.get_all_pages(croom.courses(), 'list', 'courses', page_message=page_message, + teacherId=teacherId, studentId=studentId, courseStates=courseStates, fields='nextPageToken,courses(id,name)') + else: + all_courses = [] + for course in courses: + all_courses.append(gapi.call(croom.courses(), 'get', id=course, fields='id,name')) + i = 0 + count = len(all_courses) + for course in all_courses: + i += 1 + courseId = course['id'] + if showMembers != 'students': + page_message = gapi.got_total_items_msg(f'Teachers for course {courseId}{currentCount(i, count)}', '') + teachers = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', page_message=page_message, courseId=courseId) + for teacher in teachers: + display.add_row_titles_to_csv_file(utils.flatten_json(teacher, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'TEACHER'}), csvRows, titles) + if showMembers != 'teachers': + page_message = gapi.got_total_items_msg(f'Students for course {courseId}{currentCount(i, count)}', '') + students = gapi.get_all_pages(croom.courses().students(), 'list', 'students', page_message=page_message, courseId=courseId) + for student in students: + display.add_row_titles_to_csv_file(utils.flatten_json(student, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'STUDENT'}), csvRows, titles) + display.sort_csv_titles(['courseId', 'courseName', 'userRole', 'userId'], titles) + display.write_csv_file(csvRows, titles, 'Course Participants', todrive) + +def doPrintPrintJobs(): + cp = buildGAPIObject('cloudprint') + todrive = False + titles = ['printerid', 'id'] + csvRows = [] + printerid = None + owner = None + status = None + sortorder = None + descending = False + query = None + age = None + older_or_newer = None + jobLimit = PRINTJOBS_DEFAULT_JOB_LIMIT + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg in ['olderthan', 'newerthan']: + if myarg == 'olderthan': + older_or_newer = 'older' + else: + older_or_newer = 'newer' + age_number = sys.argv[i+1][:-1] + if not age_number.isdigit(): + controlflow.system_error_exit(2, f'expected a number; got {age_number}') + age_unit = sys.argv[i+1][-1].lower() + if age_unit == 'm': + age = int(time.time()) - (int(age_number) * 60) + elif age_unit == 'h': + age = int(time.time()) - (int(age_number) * 60 * 60) + elif age_unit == 'd': + age = int(time.time()) - (int(age_number) * 60 * 60 * 24) + else: + controlflow.system_error_exit(2, f'expected m (minutes), h (hours) or d (days); got {age_unit}') + i += 2 + elif myarg == 'query': + query = sys.argv[i+1] + i += 2 + elif myarg == 'status': + status = sys.argv[i+1] + i += 2 + elif myarg == 'ascending': + descending = False + i += 1 + elif myarg == 'descending': + descending = True + i += 1 + elif myarg == 'orderby': + sortorder = sys.argv[i+1].lower().replace('_', '') + if sortorder not in PRINTJOB_ASCENDINGORDER_MAP: + controlflow.expected_argument_exit("orderby", ", ".join(PRINTJOB_ASCENDINGORDER_MAP), sortorder) + sortorder = PRINTJOB_ASCENDINGORDER_MAP[sortorder] + i += 2 + elif myarg in ['printer', 'printerid']: + printerid = sys.argv[i+1] + i += 2 + elif myarg in ['owner', 'user']: + owner = sys.argv[i+1] + i += 2 + elif myarg == 'limit': + jobLimit = getInteger(sys.argv[i+1], myarg, minVal=0) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print printjobs") + if sortorder and descending: + sortorder = PRINTJOB_DESCENDINGORDER_MAP[sortorder] + if printerid: + result = gapi.call(cp.printers(), 'get', + printerid=printerid) + checkCloudPrintResult(result) + if ((not sortorder) or (sortorder == 'CREATE_TIME_DESC')) and (older_or_newer == 'newer'): + timeExit = True + elif (sortorder == 'CREATE_TIME') and (older_or_newer == 'older'): + timeExit = True + else: + timeExit = False + jobCount = offset = 0 + while True: + if jobLimit == 0: + limit = PRINTJOBS_DEFAULT_MAX_RESULTS + else: + limit = min(PRINTJOBS_DEFAULT_MAX_RESULTS, jobLimit-jobCount) + if limit == 0: + break + result = gapi.call(cp.jobs(), 'list', + printerid=printerid, q=query, status=status, sortorder=sortorder, + owner=owner, offset=offset, limit=limit) + checkCloudPrintResult(result) + newJobs = result['range']['jobsCount'] + totalJobs = int(result['range']['jobsTotal']) + if GC_Values[GC_DEBUG_LEVEL] > 0: + sys.stderr.write(f'Debug: jobCount: {jobCount}, jobLimit: {jobLimit}, jobsCount: {newJobs}, jobsTotal: {totalJobs}\n') + if newJobs == 0: + break + jobCount += newJobs + offset += newJobs + for job in result['jobs']: + createTime = int(job['createTime'])/1000 + if older_or_newer: + if older_or_newer == 'older' and createTime > age: + if timeExit: + jobCount = totalJobs + break + continue + if older_or_newer == 'newer' and createTime < age: + if timeExit: + jobCount = totalJobs + break + continue + job['createTime'] = utils.formatTimestampYMDHMS(job['createTime']) + job['updateTime'] = utils.formatTimestampYMDHMS(job['updateTime']) + job['tags'] = ' '.join(job['tags']) + display.add_row_titles_to_csv_file(utils.flatten_json(job), csvRows, titles) + if jobCount >= totalJobs: + break + display.write_csv_file(csvRows, titles, 'Print Jobs', todrive) + +def doPrintPrinters(): + cp = buildGAPIObject('cloudprint') + todrive = False + titles = ['id',] + csvRows = [] + queries = [None] + printer_type = None + connection_status = None + extra_fields = None + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg in ['query', 'queries']: + queries = getQueries(myarg, sys.argv[i+1]) + i += 2 + elif myarg == 'type': + printer_type = sys.argv[i+1] + i += 2 + elif myarg == 'status': + connection_status = sys.argv[i+1] + i += 2 + elif myarg == 'extrafields': + extra_fields = sys.argv[i+1] + i += 2 + elif myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print printers") + for query in queries: + printers = gapi.call(cp.printers(), 'list', q=query, type=printer_type, connection_status=connection_status, extra_fields=extra_fields) + checkCloudPrintResult(printers) + for printer in printers['printers']: + printer['createTime'] = utils.formatTimestampYMDHMS(printer['createTime']) + printer['accessTime'] = utils.formatTimestampYMDHMS(printer['accessTime']) + printer['updateTime'] = utils.formatTimestampYMDHMS(printer['updateTime']) + printer['tags'] = ' '.join(printer['tags']) + display.add_row_titles_to_csv_file(utils.flatten_json(printer), csvRows, titles) + display.write_csv_file(csvRows, titles, 'Printers', todrive) + +def doPrinterShowACL(): + cp = buildGAPIObject('cloudprint') + show_printer = sys.argv[2] + printer_info = gapi.call(cp.printers(), 'get', printerid=show_printer) + checkCloudPrintResult(printer_info) + for acl in printer_info['printers'][0]['access']: + if 'key' in acl: + acl['accessURL'] = f'https://www.google.com/cloudprint/addpublicprinter.html?printerid={show_printer}&key={acl["key"]}' + display.print_json(acl) + print() + +def doPrinterAddACL(): + cp = buildGAPIObject('cloudprint') + printer = sys.argv[2] + role = sys.argv[4].upper() + scope = sys.argv[5] + notify = bool(len(sys.argv) > 6 and sys.argv[6].lower() == 'notify') + public = None + skip_notification = True + if scope.lower() == 'public': + public = True + scope = None + role = None + skip_notification = None + elif scope.find('@') == -1: + scope = f'/hd/domain/{scope}' + else: + skip_notification = not notify + result = gapi.call(cp.printers(), 'share', printerid=printer, role=role, scope=scope, public=public, skip_notification=skip_notification) + checkCloudPrintResult(result) + who = scope + if who is None: + who = 'public' + role = 'user' + print(f'Added {role} {who}') + +def doPrinterDelACL(): + cp = buildGAPIObject('cloudprint') + printer = sys.argv[2] + scope = sys.argv[4] + public = None + if scope.lower() == 'public': + public = True + scope = None + elif scope.find('@') == -1: + scope = f'/hd/domain/{scope}' + result = gapi.call(cp.printers(), 'unshare', printerid=printer, scope=scope, public=public) + checkCloudPrintResult(result) + who = scope + if who is None: + who = 'public' + print(f'Removed {who}') + +def encode_multipart(fields, files, boundary=None): + def escape_quote(s): + return s.replace('"', '\\"') + + def getFormDataLine(name, value, boundary): + return f'--{boundary}', f'Content-Disposition: form-data; name="{escape_quote(name)}"', '', str(value) + + if boundary is None: + boundary = ''.join(random.choice(ALPHANUMERIC_CHARS) for _ in range(30)) + lines = [] + for name, value in list(fields.items()): + if name == 'tags': + for tag in value: + lines.extend(getFormDataLine('tag', tag, boundary)) + else: + lines.extend(getFormDataLine(name, value, boundary)) + for name, value in list(files.items()): + filename = value['filename'] + mimetype = value['mimetype'] + lines.extend(( + f'--{boundary}', + f'Content-Disposition: form-data; name="{escape_quote(name)}"; filename="{escape_quote(filename)}"', + f'Content-Type: {mimetype}', + '', + value['content'], + )) + lines.extend(( + f'--{boundary}--', + '', + )) + body = '\r\n'.join(lines) + headers = { + 'Content-Type': f'multipart/form-data; boundary={boundary}', + 'Content-Length': str(len(body)), + } + return (body, headers) + +def doPrintJobFetch(): + cp = buildGAPIObject('cloudprint') + printerid = sys.argv[2] + if printerid == 'any': + printerid = None + owner = None + status = None + sortorder = None + descending = False + query = None + age = None + older_or_newer = None + jobLimit = PRINTJOBS_DEFAULT_JOB_LIMIT + targetFolder = os.getcwd() + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg in ['olderthan', 'newerthan']: + if myarg == 'olderthan': + older_or_newer = 'older' + else: + older_or_newer = 'newer' + age_number = sys.argv[i+1][:-1] + if not age_number.isdigit(): + controlflow.system_error_exit(2, f'expected a number; got {age_number}') + age_unit = sys.argv[i+1][-1].lower() + if age_unit == 'm': + age = int(time.time()) - (int(age_number) * 60) + elif age_unit == 'h': + age = int(time.time()) - (int(age_number) * 60 * 60) + elif age_unit == 'd': + age = int(time.time()) - (int(age_number) * 60 * 60 * 24) + else: + controlflow.system_error_exit(2, f'expected m (minutes), h (hours) or d (days); got {age_unit}') + i += 2 + elif myarg == 'query': + query = sys.argv[i+1] + i += 2 + elif myarg == 'status': + status = sys.argv[i+1] + i += 2 + elif myarg == 'ascending': + descending = False + i += 1 + elif myarg == 'descending': + descending = True + i += 1 + elif myarg == 'orderby': + sortorder = sys.argv[i+1].lower().replace('_', '') + if sortorder not in PRINTJOB_ASCENDINGORDER_MAP: + controlflow.expected_argument_exit("orderby", ", ".join(PRINTJOB_ASCENDINGORDER_MAP), sortorder) + sortorder = PRINTJOB_ASCENDINGORDER_MAP[sortorder] + i += 2 + elif myarg in ['owner', 'user']: + owner = sys.argv[i+1] + i += 2 + elif myarg == 'limit': + jobLimit = getInteger(sys.argv[i+1], myarg, minVal=0) + i += 2 + elif myarg == 'drivedir': + targetFolder = GC_Values[GC_DRIVE_DIR] + i += 1 + elif myarg == 'targetfolder': + targetFolder = os.path.expanduser(sys.argv[i+1]) + if not os.path.isdir(targetFolder): + os.makedirs(targetFolder) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam printjobs fetch") + if sortorder and descending: + sortorder = PRINTJOB_DESCENDINGORDER_MAP[sortorder] + if printerid: + result = gapi.call(cp.printers(), 'get', + printerid=printerid) + checkCloudPrintResult(result) + ssd = '{"state": {"type": "DONE"}}' + if ((not sortorder) or (sortorder == 'CREATE_TIME_DESC')) and (older_or_newer == 'newer'): + timeExit = True + elif (sortorder == 'CREATE_TIME') and (older_or_newer == 'older'): + timeExit = True + else: + timeExit = False + jobCount = offset = 0 + while True: + if jobLimit == 0: + limit = PRINTJOBS_DEFAULT_MAX_RESULTS + else: + limit = min(PRINTJOBS_DEFAULT_MAX_RESULTS, jobLimit-jobCount) + if limit == 0: + break + result = gapi.call(cp.jobs(), 'list', + printerid=printerid, q=query, status=status, sortorder=sortorder, + owner=owner, offset=offset, limit=limit) + checkCloudPrintResult(result) + newJobs = result['range']['jobsCount'] + totalJobs = int(result['range']['jobsTotal']) + if newJobs == 0: + break + jobCount += newJobs + offset += newJobs + for job in result['jobs']: + createTime = int(job['createTime'])/1000 + if older_or_newer: + if older_or_newer == 'older' and createTime > age: + if timeExit: + jobCount = totalJobs + break + continue + if older_or_newer == 'newer' and createTime < age: + if timeExit: + jobCount = totalJobs + break + continue + fileUrl = job['fileUrl'] + jobid = job['id'] + fileName = os.path.join(targetFolder, f'{"".join(c if c in FILENAME_SAFE_CHARS else "_" for c in job["title"])}-{jobid}') + _, content = cp._http.request(uri=fileUrl, method='GET') + if fileutils.write_file(fileName, content, mode='wb', continue_on_error=True): +# ticket = gapi.call(cp.jobs(), u'getticket', jobid=jobid, use_cjt=True) + result = gapi.call(cp.jobs(), 'update', jobid=jobid, semantic_state_diff=ssd) + checkCloudPrintResult(result) + print(f'Printed job {jobid} to {fileName}') + if jobCount >= totalJobs: + break + if jobCount == 0: + print('No print jobs.') + +def doDelPrinter(): + cp = buildGAPIObject('cloudprint') + printerid = sys.argv[3] + result = gapi.call(cp.printers(), 'delete', printerid=printerid) + checkCloudPrintResult(result) + +def doGetPrinterInfo(): + cp = buildGAPIObject('cloudprint') + printerid = sys.argv[3] + everything = False + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'everything': + everything = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam info printer") + result = gapi.call(cp.printers(), 'get', printerid=printerid) + checkCloudPrintResult(result) + printer_info = result['printers'][0] + printer_info['createTime'] = utils.formatTimestampYMDHMS(printer_info['createTime']) + printer_info['accessTime'] = utils.formatTimestampYMDHMS(printer_info['accessTime']) + printer_info['updateTime'] = utils.formatTimestampYMDHMS(printer_info['updateTime']) + printer_info['tags'] = ' '.join(printer_info['tags']) + if not everything: + del printer_info['capabilities'] + del printer_info['access'] + display.print_json(printer_info) + +def doUpdatePrinter(): + cp = buildGAPIObject('cloudprint') + printerid = sys.argv[3] + kwargs = {} + i = 4 + update_items = ['isTosAccepted', 'gcpVersion', 'setupUrl', + 'quotaEnabled', 'id', 'supportUrl', 'firmware', + 'currentQuota', 'type', 'public', 'status', 'description', + 'defaultDisplayName', 'proxy', 'dailyQuota', 'manufacturer', + 'displayName', 'name', 'uuid', 'updateUrl', 'ownerId', 'model'] + while i < len(sys.argv): + arg_in_item = False + for item in update_items: + if item.lower() == sys.argv[i].lower(): + kwargs[item] = sys.argv[i+1] + i += 2 + arg_in_item = True + break + if not arg_in_item: + controlflow.invalid_argument_exit(sys.argv[i], "gam update printer") + result = gapi.call(cp.printers(), 'update', printerid=printerid, **kwargs) + checkCloudPrintResult(result) + print(f'Updated printer {printerid}') + +def doPrinterRegister(): + cp = buildGAPIObject('cloudprint') + form_fields = {'name': 'GAM', + 'proxy': 'GAM', + 'uuid': _getValueFromOAuth('sub'), + 'manufacturer': gam_author, + 'model': 'cp1', + 'gcp_version': '2.0', + 'setup_url': GAM_URL, + 'support_url': 'https://groups.google.com/forum/#!forum/google-apps-manager', + 'update_url': GAM_RELEASES, + 'firmware': gam_version, + 'semantic_state': {"version": "1.0", "printer": {"state": "IDLE",}}, + 'use_cdd': True, + 'capabilities': {"version": "1.0", + "printer": {"supported_content_type": [{"content_type": "application/pdf", "min_version": "1.5"}, + {"content_type": "image/jpeg"}, + {"content_type": "text/plain"} + ], + "copies": {"default": 1, "max": 100}, + "media_size": {"option": [{"name": "ISO_A4", "width_microns": 210000, "height_microns": 297000}, + {"name": "NA_LEGAL", "width_microns": 215900, "height_microns": 355600}, + {"name": "NA_LETTER", "width_microns": 215900, "height_microns": 279400, "is_default": True} + ], + }, + }, + }, + 'tags': ['GAM', GAM_URL], + } + body, headers = encode_multipart(form_fields, {}) + #Get the printer first to make sure our OAuth access token is fresh + gapi.call(cp.printers(), 'list') + _, result = cp._http.request(uri='https://www.google.com/cloudprint/register', method='POST', body=body, headers=headers) + result = json.loads(result.decode(UTF8)) + checkCloudPrintResult(result) + print(f'Created printer {result["printers"][0]["id"]}') + +def doPrintJobResubmit(): + cp = buildGAPIObject('cloudprint') + jobid = sys.argv[2] + printerid = sys.argv[4] + ssd = '{"state": {"type": "HELD"}}' + result = gapi.call(cp.jobs(), 'update', jobid=jobid, semantic_state_diff=ssd) + checkCloudPrintResult(result) + ticket = gapi.call(cp.jobs(), 'getticket', jobid=jobid, use_cjt=True) + result = gapi.call(cp.jobs(), 'resubmit', printerid=printerid, jobid=jobid, ticket=ticket) + checkCloudPrintResult(result) + print(f'Success resubmitting {jobid} as job {result["job"]["id"]} to printer {printerid}') + +def doPrintJobSubmit(): + cp = buildGAPIObject('cloudprint') + printer = sys.argv[2] + content = sys.argv[4] + form_fields = {'printerid': printer, + 'title': content, + 'ticket': '{"version": "1.0"}', + 'tags': ['GAM', GAM_URL]} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'tag': + form_fields['tags'].append(sys.argv[i+1]) + i += 2 + elif myarg in ['name', 'title']: + form_fields['title'] = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam printer ... print") + form_files = {} + if content[:4] == 'http': + form_fields['content'] = content + form_fields['contentType'] = 'url' + else: + filepath = content + content = os.path.basename(content) + mimetype = mimetypes.guess_type(filepath)[0] + if mimetype is None: + mimetype = 'application/octet-stream' + filecontent = fileutils.read_file(filepath, mode='rb') + form_files['content'] = {'filename': content, 'content': filecontent, 'mimetype': mimetype} + #result = gapi.call(cp.printers(), u'submit', body=body) + body, headers = encode_multipart(form_fields, form_files) + #Get the printer first to make sure our OAuth access token is fresh + gapi.call(cp.printers(), 'get', printerid=printer) + _, result = cp._http.request(uri='https://www.google.com/cloudprint/submit', method='POST', body=body, headers=headers) + result = json.loads(result.decode(UTF8)) + checkCloudPrintResult(result) + print(f'Submitted print job {result["job"]["id"]}') + +def doDeletePrintJob(): + cp = buildGAPIObject('cloudprint') + job = sys.argv[2] + result = gapi.call(cp.jobs(), 'delete', jobid=job) + checkCloudPrintResult(result) + print(f'Print Job {job} deleted') + +def doCancelPrintJob(): + cp = buildGAPIObject('cloudprint') + job = sys.argv[2] + ssd = '{"state": {"type": "ABORTED", "user_action_cause": {"action_code": "CANCELLED"}}}' + result = gapi.call(cp.jobs(), 'update', jobid=job, semantic_state_diff=ssd) + checkCloudPrintResult(result) + print(f'Print Job {job} cancelled') + +def checkCloudPrintResult(result): + if isinstance(result, bytes): + result = result.decode(UTF8) + if isinstance(result, str): + try: + result = json.loads(result) + except ValueError: + controlflow.system_error_exit(3, f'unexpected response: {result}') + if not result['success']: + controlflow.system_error_exit(result['errorCode'], f'{result["errorCode"]}: {result["message"]}') + +def doProfile(users): + cd = buildGAPIObject('directory') + myarg = sys.argv[4].lower() + if myarg in ['share', 'shared']: + body = {'includeInGlobalAddressList': True} + elif myarg in ['unshare', 'unshared']: + body = {'includeInGlobalAddressList': False} + else: + controlflow.expected_argument_exit('value for "gam profile"', ", ".join(["share", "shared", "unshare", "unshared"]), sys.argv[4]) + i = 0 + count = len(users) + for user in users: + i += 1 + print(f'Setting Profile Sharing to {body["includeInGlobalAddressList"]} for {user}{currentCount(i, count)}') + gapi.call(cd.users(), 'update', soft_errors=True, userKey=user, body=body) + +def showProfile(users): + cd = buildGAPIObject('directory') + i = 0 + count = len(users) + for user in users: + i += 1 + result = gapi.call(cd.users(), 'get', userKey=user, fields='includeInGlobalAddressList') + try: + print(f'User: {user} Profile Shared: {result["includeInGlobalAddressList"]}{currentCount(i, count)}') + except IndexError: + pass + +def doPhoto(users): + cd = buildGAPIObject('directory') + i = 0 + count = len(users) + for user in users: + i += 1 + filename = sys.argv[5].replace('#user#', user) + filename = filename.replace('#email#', user) + filename = filename.replace('#username#', user[:user.find('@')]) + print(f'Updating photo for {user} with {filename}{currentCount(i, count)}') + if re.match('^(ht|f)tps?://.*$', filename): + simplehttp = transport.create_http() + try: + (_, image_data) = simplehttp.request(filename, 'GET') + except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError) as e: + print(e) + continue + else: + image_data = fileutils.read_file(filename, mode='rb', continue_on_error=True, display_errors=True) + if image_data is None: + continue + body = {'photoData': base64.urlsafe_b64encode(image_data).decode(UTF8)} + gapi.call(cd.users().photos(), 'update', soft_errors=True, userKey=user, body=body) + +def getPhoto(users): + cd = buildGAPIObject('directory') + targetFolder = os.getcwd() + showPhotoData = True + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'drivedir': + targetFolder = GC_Values[GC_DRIVE_DIR] + i += 1 + elif myarg == 'targetfolder': + targetFolder = os.path.expanduser(sys.argv[i+1]) + if not os.path.isdir(targetFolder): + os.makedirs(targetFolder) + i += 2 + elif myarg == 'noshow': + showPhotoData = False + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam get photo") + i = 0 + count = len(users) + for user in users: + i += 1 + filename = os.path.join(targetFolder, f'{user}.jpg') + print(f'Saving photo to {filename}{currentCount(i, count)}') + try: + photo = gapi.call(cd.users().photos(), 'get', throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND, gapi_errors.ErrorReason.RESOURCE_NOT_FOUND], userKey=user) + except gapi_errors.GapiUserNotFoundError: + print(f' unknown user {user}') + continue + except gapi_errors.GapiResourceNotFoundError: + print(f' no photo for {user}') + continue + try: + photo_data = photo['photoData'] + if showPhotoData: + print(photo_data) + except KeyError: + print(f' no photo for {user}') + continue + decoded_photo_data = base64.urlsafe_b64decode(photo_data) + fileutils.write_file(filename, decoded_photo_data, mode='wb', continue_on_error=True) + +def deletePhoto(users): + cd = buildGAPIObject('directory') + i = 0 + count = len(users) + for user in users: + i += 1 + print(f'Deleting photo for {user}{currentCount(i, count)}') + gapi.call(cd.users().photos(), 'delete', userKey=user) + +def printDriveSettings(users): + todrive = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show drivesettings") + dont_show = ['kind', 'exportFormats', 'importFormats', 'maxUploadSize', 'maxImportSizes', 'user', 'appInstalled'] + csvRows = [] + titles = ['email',] + i = 0 + count = len(users) + for user in users: + i += 1 + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + sys.stderr.write(f'Getting Drive settings for {user}{currentCountNL(i, count)}') + feed = gapi.call(drive.about(), 'get', fields='*', soft_errors=True) + if feed is None: + continue + row = {'email': user} + for setting in feed: + if setting in dont_show: + continue + if setting == 'storageQuota': + for subsetting, value in feed[setting].items(): + row[subsetting] = f'{int(value) / 1024 / 1024}mb' + if subsetting not in titles: + titles.append(subsetting) + continue + row[setting] = feed[setting] + if setting not in titles: + titles.append(setting) + csvRows.append(row) + display.write_csv_file(csvRows, titles, 'User Drive Settings', todrive) + +def getTeamDriveThemes(users): + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + themes = gapi.call(drive.about(), 'get', fields='teamDriveThemes', soft_errors=True) + if themes is None or 'teamDriveThemes' not in themes: + continue + print('theme') + for theme in themes['teamDriveThemes']: + print(theme['id']) + +def printDriveActivity(users): + drive_ancestorId = 'root' + drive_fileId = None + todrive = False + titles = ['user.name', 'user.permissionId', 'target.id', 'target.name', 'target.mimeType'] + csvRows = [] + i = 5 + while i < len(sys.argv): + activity_object = sys.argv[i].lower().replace('_', '') + if activity_object == 'fileid': + drive_fileId = sys.argv[i+1] + drive_ancestorId = None + i += 2 + elif activity_object == 'folderid': + drive_ancestorId = sys.argv[i+1] + i += 2 + elif activity_object == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show driveactivity") + for user in users: + user, activity = buildActivityGAPIObject(user) + if not activity: + continue + page_message = gapi.got_total_items_msg(f'Activities for {user}', '') + feed = gapi.get_all_pages(activity.activities(), 'list', 'activities', + page_message=page_message, source='drive.google.com', userId='me', + drive_ancestorId=drive_ancestorId, groupingStrategy='none', + drive_fileId=drive_fileId) + for item in feed: + display.add_row_titles_to_csv_file(utils.flatten_json(item['combinedEvent']), csvRows, titles) + display.write_csv_file(csvRows, titles, 'Drive Activity', todrive) + +def printPermission(permission): + if 'name' in permission: + print(permission['name']) + elif 'id' in permission: + if permission['id'] == 'anyone': + print('Anyone') + elif permission['id'] == 'anyoneWithLink': + print('Anyone with Link') + else: + print(permission['id']) + for key in permission: + if key in ['name', 'kind', 'etag', 'selfLink',]: + continue + print(f' {key}: {permission[key]}') + +def showDriveFileACL(users): + fileId = sys.argv[5] + useDomainAdminAccess = False + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show drivefileacl") + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + feed = gapi.get_all_pages(drive.permissions(), 'list', 'permissions', + fileId=fileId, fields='*', supportsAllDrives=True, + useDomainAdminAccess=useDomainAdminAccess) + for permission in feed: + printPermission(permission) + print('') + +def getPermissionId(argstr): + permissionId = argstr.strip() + cg = UID_PATTERN.match(permissionId) + if cg: + return cg.group(1) + permissionId = argstr.lower() + if permissionId == 'anyone': + return 'anyone' + if permissionId == 'anyonewithlink': + return 'anyoneWithLink' + if permissionId.find('@') == -1: + permissionId = f'{permissionId}@{GC_Values[GC_DOMAIN].lower()}' + # We have to use v2 here since v3 has no permissions.getIdForEmail equivalent + # https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4313 + _, drive2 = buildDriveGAPIObject(_getValueFromOAuth('email')) + return gapi.call(drive2.permissions(), 'getIdForEmail', email=permissionId, fields='id')['id'] + +def delDriveFileACL(users): + fileId = sys.argv[5] + permissionId = getPermissionId(sys.argv[6]) + useDomainAdminAccess = False + i = 7 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam delete drivefileacl") + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + print(f'Removing permission for {permissionId} from {fileId}') + gapi.call(drive.permissions(), 'delete', fileId=fileId, + permissionId=permissionId, supportsAllDrives=True, + useDomainAdminAccess=useDomainAdminAccess) + +DRIVEFILE_ACL_ROLES_MAP = { + 'commenter': 'commenter', + 'contentmanager': 'fileOrganizer', + 'editor': 'writer', + 'fileorganizer': 'fileOrganizer', + 'organizer': 'organizer', + 'owner': 'owner', + 'read': 'reader', + 'reader': 'reader', + 'writer': 'writer', + } + +def addDriveFileACL(users): + fileId = sys.argv[5] + body = {'type': sys.argv[6].lower()} + sendNotificationEmail = False + emailMessage = None + transferOwnership = None + useDomainAdminAccess = False + if body['type'] == 'anyone': + i = 7 + elif body['type'] in ['user', 'group']: + body['emailAddress'] = normalizeEmailAddressOrUID(sys.argv[7]) + i = 8 + elif body['type'] == 'domain': + body['domain'] = sys.argv[7] + i = 8 + else: + controlflow.expected_argument_exit("permission type", ", ".join(["user", "group", "domain", "anyone"]), body['type']) + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'withlink': + body['allowFileDiscovery'] = False + i += 1 + elif myarg == 'discoverable': + body['allowFileDiscovery'] = True + i += 1 + elif myarg == 'role': + role = sys.argv[i+1].lower() + if role not in DRIVEFILE_ACL_ROLES_MAP: + controlflow.expected_argument_exit("role", ", ".join(DRIVEFILE_ACL_ROLES_MAP), role) + body['role'] = DRIVEFILE_ACL_ROLES_MAP[role] + if body['role'] == 'owner': + sendNotificationEmail = True + transferOwnership = True + i += 2 + elif myarg == 'sendemail': + sendNotificationEmail = True + i += 1 + elif myarg == 'emailmessage': + sendNotificationEmail = True + emailMessage = sys.argv[i+1] + i += 2 + elif myarg == 'expires': + body['expirationTime'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) + i += 2 + elif myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam add drivefileacl") + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + result = gapi.call(drive.permissions(), 'create', fields='*', + fileId=fileId, sendNotificationEmail=sendNotificationEmail, + emailMessage=emailMessage, body=body, supportsAllDrives=True, + transferOwnership=transferOwnership, + useDomainAdminAccess=useDomainAdminAccess) + printPermission(result) + +def updateDriveFileACL(users): + fileId = sys.argv[5] + permissionId = getPermissionId(sys.argv[6]) + transferOwnership = None + removeExpiration = None + useDomainAdminAccess = False + body = {} + i = 7 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'removeexpiration': + removeExpiration = True + i += 1 + elif myarg == 'role': + role = sys.argv[i+1].lower() + if role not in DRIVEFILE_ACL_ROLES_MAP: + controlflow.expected_argument_exit("role", ", ".join(DRIVEFILE_ACL_ROLES_MAP), role) + body['role'] = DRIVEFILE_ACL_ROLES_MAP[role] + if body['role'] == 'owner': + transferOwnership = True + i += 2 + elif myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam update drivefileacl") + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + print(f'updating permissions for {permissionId} to file {fileId}') + result = gapi.call(drive.permissions(), 'update', fields='*', + fileId=fileId, permissionId=permissionId, removeExpiration=removeExpiration, + transferOwnership=transferOwnership, body=body, + supportsAllDrives=True, useDomainAdminAccess=useDomainAdminAccess) + printPermission(result) + +def _stripMeInOwners(query): + if not query: + return query + if query == "'me' in owners": + return None + if query.startswith("'me' in owners and "): + return query[len("'me' in owners and "):] + return query + +def printDriveFileList(users): + allfields = anyowner = todrive = False + fieldsList = [] + fieldsTitles = {} + labelsList = [] + orderByList = [] + titles = ['Owner',] + csvRows = [] + query = "'me' in owners" + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'orderby': + fieldName = sys.argv[i+1].lower() + i += 2 + if fieldName in DRIVEFILE_ORDERBY_CHOICES_MAP: + fieldName = DRIVEFILE_ORDERBY_CHOICES_MAP[fieldName] + orderBy = '' + if i < len(sys.argv): + orderBy = sys.argv[i].lower() + if orderBy in SORTORDER_CHOICES_MAP: + orderBy = SORTORDER_CHOICES_MAP[orderBy] + i += 1 + if orderBy != 'DESCENDING': + orderByList.append(fieldName) + else: + orderByList.append(f'{fieldName} desc') + else: + controlflow.expected_argument_exit("orderby", ", ".join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)), fieldName) + elif myarg == 'query': + query += f' and {sys.argv[i+1]}' + i += 2 + elif myarg == 'fullquery': + query = sys.argv[i+1] + i += 2 + elif myarg == 'anyowner': + anyowner = True + i += 1 + elif myarg == 'allfields': + fieldsList = [] + allfields = True + i += 1 + elif myarg in DRIVEFILE_FIELDS_CHOICES_MAP: + display.add_field_to_csv_file(myarg, {myarg: [DRIVEFILE_FIELDS_CHOICES_MAP[myarg]]}, fieldsList, fieldsTitles, titles) + i += 1 + elif myarg in DRIVEFILE_LABEL_CHOICES_MAP: + display.add_field_to_csv_file(myarg, {myarg: [DRIVEFILE_LABEL_CHOICES_MAP[myarg]]}, labelsList, fieldsTitles, titles) + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam show filelist") + if fieldsList or labelsList: + fields = 'nextPageToken,items(' + if fieldsList: + fields += ','.join(set(fieldsList)) + if labelsList: + fields += ',' + if labelsList: + fields += f'labels({",".join(set(labelsList))})' + fields += ')' + elif not allfields: + for field in ['name', 'alternatelink']: + display.add_field_to_csv_file(field, {field: [DRIVEFILE_FIELDS_CHOICES_MAP[field]]}, fieldsList, fieldsTitles, titles) + fields = f'nextPageToken,items({",".join(set(fieldsList))})' + else: + fields = '*' + if orderByList: + orderBy = ','.join(orderByList) + else: + orderBy = None + if anyowner: + query = _stripMeInOwners(query) + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + sys.stderr.write(f'Getting files for {user}...\n') + page_message = gapi.got_total_items_msg(f'Files for {user}', '...\n') + feed = gapi.get_all_pages(drive.files(), 'list', 'items', + page_message=page_message, soft_errors=True, + q=query, orderBy=orderBy, fields=fields) + for f_file in feed: + a_file = {'Owner': user} + for attrib in f_file: + if attrib in ['kind', 'etag']: + continue + if not isinstance(f_file[attrib], dict): + if isinstance(f_file[attrib], list): + if f_file[attrib]: + if isinstance(f_file[attrib][0], (str, int, bool)): + if attrib not in titles: + titles.append(attrib) + a_file[attrib] = ' '.join(f_file[attrib]) + else: + for j, l_attrib in enumerate(f_file[attrib]): + for list_attrib in l_attrib: + if list_attrib in ['kind', 'etag', 'selfLink']: + continue + x_attrib = f'{attrib}.{j}.{list_attrib}' + if x_attrib not in titles: + titles.append(x_attrib) + a_file[x_attrib] = l_attrib[list_attrib] + elif isinstance(f_file[attrib], (str, int, bool)): + if attrib not in titles: + titles.append(attrib) + a_file[attrib] = f_file[attrib] + else: + sys.stderr.write(f'File ID: {f_file["id"]}, Attribute: {attrib}, Unknown type: {type(f_file[attrib])}\n') + elif attrib == 'labels': + for dict_attrib in f_file[attrib]: + if dict_attrib not in titles: + titles.append(dict_attrib) + a_file[dict_attrib] = f_file[attrib][dict_attrib] + else: + for dict_attrib in f_file[attrib]: + if dict_attrib in ['kind', 'etag']: + continue + x_attrib = f'{attrib}.{dict_attrib}' + if x_attrib not in titles: + titles.append(x_attrib) + a_file[x_attrib] = f_file[attrib][dict_attrib] + csvRows.append(a_file) + if allfields: + display.sort_csv_titles(['Owner', 'id', 'title'], titles) + display.write_csv_file(csvRows, titles, f'{sys.argv[1]} {sys.argv[2]} Drive Files', todrive) + +def doDriveSearch(drive, query=None, quiet=False): + if not quiet: + print(f'Searching for files with query: "{query}"...') + page_message = gapi.got_total_items_msg('Files', '...\n') + else: + page_message = None + files = gapi.get_all_pages(drive.files(), 'list', 'items', + page_message=page_message, + q=query, fields='nextPageToken,items(id)') + ids = list() + for f_file in files: + ids.append(f_file['id']) + return ids + +def getFileIdFromAlternateLink(altLink): + loc = altLink.find('/d/') + if loc > 0: + fileId = altLink[loc+3:] + loc = fileId.find('/') + if loc != -1: + return fileId[:loc] + else: + loc = altLink.find('/folderview?id=') + if loc > 0: + fileId = altLink[loc+15:] + loc = fileId.find('&') + if loc != -1: + return fileId[:loc] + controlflow.system_error_exit(2, f'{altLink} is not a valid Drive File alternateLink') + +def deleteDriveFile(users): + fileIds = sys.argv[5] + function = 'trash' + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'purge': + function = 'delete' + i += 1 + elif myarg == 'untrash': + function = 'untrash' + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam delete drivefile") + action = DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP[function] + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + if fileIds[:6].lower() == 'query:': + file_ids = doDriveSearch(drive, query=fileIds[6:]) + else: + if fileIds[:8].lower() == 'https://' or fileIds[:7].lower() == 'http://': + fileIds = getFileIdFromAlternateLink(fileIds) + file_ids = [fileIds,] + if not file_ids: + print(f'No files to {function} for {user}') + j = 0 + batch_size = 10 + dbatch = drive.new_batch_http_request(callback=drive_del_result) + method = getattr(drive.files(), function) + for fileId in file_ids: + j += 1 + dbatch.add(method(fileId=fileId, supportsAllDrives=True)) + if len(dbatch._order) == batch_size: + print(f'{action} {len(dbatch._order)} files...') + dbatch.execute() + dbatch = drive.new_batch_http_request(callback=drive_del_result) + if len(dbatch._order) > 0: + print(f'{action} {len(dbatch._order)} files...') + dbatch.execute() + +def drive_del_result(request_id, response, exception): + if exception: + print(exception) + +def printDriveFolderContents(feed, folderId, indent): + for f_file in feed: + for parent in f_file['parents']: + if folderId == parent['id']: + print(' ' * indent, f_file['title']) + if f_file['mimeType'] == 'application/vnd.google-apps.folder': + printDriveFolderContents(feed, f_file['id'], indent+1) + break + +def showDriveFileTree(users): + anyowner = False + orderByList = [] + query = "'me' in owners" + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'anyowner': + anyowner = True + i += 1 + elif myarg == 'orderby': + fieldName = sys.argv[i+1].lower() + i += 2 + if fieldName in DRIVEFILE_ORDERBY_CHOICES_MAP: + fieldName = DRIVEFILE_ORDERBY_CHOICES_MAP[fieldName] + orderBy = '' + if i < len(sys.argv): + orderBy = sys.argv[i].lower() + if orderBy in SORTORDER_CHOICES_MAP: + orderBy = SORTORDER_CHOICES_MAP[orderBy] + i += 1 + if orderBy != 'DESCENDING': + orderByList.append(fieldName) + else: + orderByList.append(f'{fieldName} desc') + else: + controlflow.expected_argument_exit("orderby", ", ".join(sorted(DRIVEFILE_ORDERBY_CHOICES_MAP)), fieldName) + else: + controlflow.invalid_argument_exit(myarg, "gam show filetree") + if orderByList: + orderBy = ','.join(orderByList) + else: + orderBy = None + if anyowner: + query = _stripMeInOwners(query) + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + root_folder = gapi.call(drive.about(), 'get', fields='rootFolderId')['rootFolderId'] + sys.stderr.write(f'Getting all files for {user}...\n') + page_message = gapi.got_total_items_msg(f'Files for {user}', '...\n') + feed = gapi.get_all_pages(drive.files(), 'list', 'items', page_message=page_message, + q=query, orderBy=orderBy, fields='items(id,title,parents(id),mimeType),nextPageToken') + printDriveFolderContents(feed, root_folder, 0) + +def deleteEmptyDriveFolders(users): + query = '"me" in owners and mimeType = "application/vnd.google-apps.folder"' + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + deleted_empty = True + while deleted_empty: + sys.stderr.write(f'Getting folders for {user}...\n') + page_message = gapi.got_total_items_msg(f'Folders for {user}', '...\n') + feed = gapi.get_all_pages(drive.files(), 'list', 'items', page_message=page_message, + q=query, fields='items(title,id),nextPageToken') + deleted_empty = False + for folder in feed: + children = gapi.call(drive.children(), 'list', + folderId=folder['id'], fields='items(id)', maxResults=1) + if 'items' not in children or not children['items']: + print(f' deleting empty folder {folder["title"]}...') + gapi.call(drive.files(), 'delete', fileId=folder['id']) + deleted_empty = True + else: + print(f' not deleting folder {folder["title"]} because it contains at least 1 item ({children["items"][0]["id"]})') + +def doEmptyDriveTrash(users): + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + print(f'Emptying Drive trash for {user}') + gapi.call(drive.files(), 'emptyTrash') + +def escapeDriveFileName(filename): + if filename.find("'") == -1 and filename.find('\\') == -1: + return filename + encfilename = '' + for c in filename: + if c == "'": + encfilename += "\\'" + elif c == '\\': + encfilename += '\\\\' + else: + encfilename += c + return encfilename + +def initializeDriveFileAttributes(): + return ({}, {DFA_LOCALFILEPATH: None, DFA_LOCALFILENAME: None, DFA_LOCALMIMETYPE: None, DFA_CONVERT: None, DFA_OCR: None, DFA_OCRLANGUAGE: None, DFA_PARENTQUERY: None}) + +def getDriveFileAttribute(i, body, parameters, myarg, update=False): + if myarg == 'localfile': + parameters[DFA_LOCALFILEPATH] = sys.argv[i+1] + parameters[DFA_LOCALFILENAME] = os.path.basename(parameters[DFA_LOCALFILEPATH]) + body.setdefault('title', parameters[DFA_LOCALFILENAME]) + body['mimeType'] = mimetypes.guess_type(parameters[DFA_LOCALFILEPATH])[0] + if body['mimeType'] is None: + body['mimeType'] = 'application/octet-stream' + parameters[DFA_LOCALMIMETYPE] = body['mimeType'] + i += 2 + elif myarg == 'convert': + parameters[DFA_CONVERT] = True + i += 1 + elif myarg == 'ocr': + parameters[DFA_OCR] = True + i += 1 + elif myarg == 'ocrlanguage': + parameters[DFA_OCRLANGUAGE] = LANGUAGE_CODES_MAP.get(sys.argv[i+1].lower(), sys.argv[i+1]) + i += 2 + elif myarg in ['copyrequireswriterpermission', 'restrict', 'restricted']: + if update: + body['copyRequiresWriterPermission'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + else: + body['copyRequiresWriterPermission'] = True + i += 1 + elif myarg in DRIVEFILE_LABEL_CHOICES_MAP: + body.setdefault('labels', {}) + if update: + body['labels'][DRIVEFILE_LABEL_CHOICES_MAP[myarg]] = getBoolean(sys.argv[i+1], myarg) + i += 2 + else: + body['labels'][DRIVEFILE_LABEL_CHOICES_MAP[myarg]] = True + i += 1 + elif myarg in ['lastviewedbyme', 'lastviewedbyuser', 'lastviewedbymedate', 'lastviewedbymetime']: + body['lastViewedByMeDate'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) + i += 2 + elif myarg in ['modifieddate', 'modifiedtime']: + body['modifiedDate'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) + i += 2 + elif myarg == 'description': + body['description'] = sys.argv[i+1] + i += 2 + elif myarg == 'mimetype': + mimeType = sys.argv[i+1] + if mimeType in MIMETYPE_CHOICES_MAP: + body['mimeType'] = MIMETYPE_CHOICES_MAP[mimeType] + else: + controlflow.expected_argument_exit("mimetype", ", ".join(MIMETYPE_CHOICES_MAP), mimeType) + i += 2 + elif myarg == 'parentid': + body.setdefault('parents', []) + body['parents'].append({'id': sys.argv[i+1]}) + i += 2 + elif myarg == 'parentname': + parameters[DFA_PARENTQUERY] = f"'me' in owners and mimeType = '{MIMETYPE_GA_FOLDER}' and title = '{escapeDriveFileName(sys.argv[i+1])}'" + i += 2 + elif myarg in ['anyownerparentname']: + parameters[DFA_PARENTQUERY] = f"mimeType = '{MIMETYPE_GA_FOLDER}' and title = '{escapeDriveFileName(sys.argv[i+1])}'" + i += 2 + elif myarg == 'writerscantshare': + body['writersCanShare'] = False + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['add', 'update'][update]} drivefile") + return i + +def doUpdateDriveFile(users): + fileIdSelection = {'fileIds': [], 'query': None} + media_body = None + operation = 'update' + body, parameters = initializeDriveFileAttributes() + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'copy': + operation = 'copy' + i += 1 + elif myarg == 'newfilename': + body['title'] = sys.argv[i+1] + i += 2 + elif myarg == 'id': + fileIdSelection['fileIds'] = [sys.argv[i+1],] + i += 2 + elif myarg == 'query': + fileIdSelection['query'] = sys.argv[i+1] + i += 2 + elif myarg == 'drivefilename': + fileIdSelection['query'] = f"'me' in owners and title = '{sys.argv[i+1]}'" + i += 2 + else: + i = getDriveFileAttribute(i, body, parameters, myarg, True) + if not fileIdSelection['query'] and not fileIdSelection['fileIds']: + controlflow.system_error_exit(2, 'you need to specify either id, query or drivefilename in order to determine the file(s) to update') + if fileIdSelection['query'] and fileIdSelection['fileIds']: + controlflow.system_error_exit(2, 'you cannot specify multiple file identifiers. Choose one of id, drivefilename, query.') + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + if parameters[DFA_PARENTQUERY]: + more_parents = doDriveSearch(drive, query=parameters[DFA_PARENTQUERY]) + body.setdefault('parents', []) + for a_parent in more_parents: + body['parents'].append({'id': a_parent}) + if fileIdSelection['query']: + fileIdSelection['fileIds'] = doDriveSearch(drive, query=fileIdSelection['query']) + if not fileIdSelection['fileIds']: + print(f'No files to {operation} for {user}') + continue + if operation == 'update': + if parameters[DFA_LOCALFILEPATH]: + media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True) + for fileId in fileIdSelection['fileIds']: + if media_body: + result = gapi.call(drive.files(), 'update', + fileId=fileId, convert=parameters[DFA_CONVERT], + ocr=parameters[DFA_OCR], + ocrLanguage=parameters[DFA_OCRLANGUAGE], + media_body=media_body, body=body, fields='id', + supportsAllDrives=True) + print(f'Successfully updated {result["id"]} drive file with content from {parameters[DFA_LOCALFILENAME]}') + else: + result = gapi.call(drive.files(), 'patch', + fileId=fileId, convert=parameters[DFA_CONVERT], + ocr=parameters[DFA_OCR], + ocrLanguage=parameters[DFA_OCRLANGUAGE], body=body, + fields='id', supportsAllDrives=True) + print(f'Successfully updated drive file/folder ID {result["id"]}') + else: + for fileId in fileIdSelection['fileIds']: + result = gapi.call(drive.files(), 'copy', + fileId=fileId, convert=parameters[DFA_CONVERT], + ocr=parameters[DFA_OCR], + ocrLanguage=parameters[DFA_OCRLANGUAGE], + body=body, fields='id', supportsAllDrives=True) + print(f'Successfully copied {fileId} to {result["id"]}') + +def createDriveFile(users): + csv_output = return_id_only = to_drive = False + csv_rows = [] + csv_titles = ['User', 'title', 'id'] + media_body = None + body, parameters = initializeDriveFileAttributes() + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'drivefilename': + body['title'] = sys.argv[i+1] + i += 2 + elif myarg == 'csv': + csv_output = True + i += 1 + elif myarg == 'todrive': + to_drive = True + i += 1 + elif myarg == 'returnidonly': + return_id_only = True + i += 1 + else: + i = getDriveFileAttribute(i, body, parameters, myarg, False) + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + if parameters[DFA_PARENTQUERY]: + more_parents = doDriveSearch(drive, query=parameters[DFA_PARENTQUERY]) + body.setdefault('parents', []) + for a_parent in more_parents: + body['parents'].append({'id': a_parent}) + if parameters[DFA_LOCALFILEPATH]: + media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True) + result = gapi.call(drive.files(), 'insert', + convert=parameters[DFA_CONVERT], ocr=parameters[DFA_OCR], + ocrLanguage=parameters[DFA_OCRLANGUAGE], + media_body=media_body, body=body, fields='id,title,mimeType', + supportsAllDrives=True) + if return_id_only: + sys.stdout.write(f"{result['id']}\n") + elif csv_output: + csv_rows.append({'User': user, 'title': result['title'], 'id': result['id']}) + else: + titleInfo = f'{result["title"]}({result["id"]})' + if parameters[DFA_LOCALFILENAME]: + print(f'Successfully uploaded {parameters[DFA_LOCALFILENAME]} to Drive File {titleInfo}') + else: + created_type = ['Folder', 'File'][result['mimeType'] != MIMETYPE_GA_FOLDER] + print(f'Successfully created Drive {created_type} {titleInfo}') + if csv_output: + display.write_csv_file(csv_rows, csv_titles, 'Files', to_drive) + +HTTP_ERROR_PATTERN = re.compile(r'^.*returned "(.*)">$') + +def downloadDriveFile(users): + i = 5 + fileIdSelection = {'fileIds': [], 'query': None} + csvSheetTitle = revisionId = None + exportFormatName = 'openoffice' + exportFormatChoices = [exportFormatName] + exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName] + targetFolder = GC_Values[GC_DRIVE_DIR] + targetName = None + overwrite = showProgress = targetStdout = False + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'id': + fileIdSelection['fileIds'] = [sys.argv[i+1],] + i += 2 + elif myarg == 'query': + fileIdSelection['query'] = sys.argv[i+1] + i += 2 + elif myarg == 'drivefilename': + fileIdSelection['query'] = f"'me' in owners and title = '{sys.argv[i+1]}'" + i += 2 + elif myarg == 'revision': + revisionId = getInteger(sys.argv[i+1], myarg, minVal=1) + i += 2 + elif myarg == 'csvsheet': + csvSheetTitle = sys.argv[i+1] + csvSheetTitleLower = csvSheetTitle.lower() + i += 2 + elif myarg == 'format': + exportFormatChoices = sys.argv[i+1].replace(',', ' ').lower().split() + exportFormats = [] + for exportFormat in exportFormatChoices: + if exportFormat in DOCUMENT_FORMATS_MAP: + exportFormats.extend(DOCUMENT_FORMATS_MAP[exportFormat]) + else: + controlflow.expected_argument_exit("format", ", ".join(DOCUMENT_FORMATS_MAP), exportFormat) + i += 2 + elif myarg == 'targetfolder': + targetFolder = os.path.expanduser(sys.argv[i+1]) + if not os.path.isdir(targetFolder): + os.makedirs(targetFolder) + i += 2 + elif myarg == 'targetname': + targetName = sys.argv[i+1] + targetStdout = targetName == '-' + i += 2 + elif myarg == 'overwrite': + overwrite = True + i += 1 + elif myarg == 'showprogress': + showProgress = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam get drivefile") + if not fileIdSelection['query'] and not fileIdSelection['fileIds']: + controlflow.system_error_exit(2, 'you need to specify either id, query or drivefilename in order to determine the file(s) to download') + if fileIdSelection['query'] and fileIdSelection['fileIds']: + controlflow.system_error_exit(2, 'you cannot specify multiple file identifiers. Choose one of id, drivefilename, query.') + if csvSheetTitle: + exportFormatName = 'csv' + exportFormatChoices = [exportFormatName] + exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName] + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + if csvSheetTitle: + sheet = buildGAPIServiceObject('sheets', user) + if not sheet: + continue + if fileIdSelection['query']: + fileIdSelection['fileIds'] = doDriveSearch(drive, query=fileIdSelection['query'], quiet=targetStdout) + else: + fileId = fileIdSelection['fileIds'][0] + if fileId[:8].lower() == 'https://' or fileId[:7].lower() == 'http://': + fileIdSelection['fileIds'][0] = getFileIdFromAlternateLink(fileId) + if not fileIdSelection['fileIds']: + print(f'No files to download for {user}') + i = 0 + for fileId in fileIdSelection['fileIds']: + fileExtension = None + result = gapi.call(drive.files(), 'get', + fileId=fileId, fields='fileExtension,fileSize,mimeType,title', supportsAllDrives=True) + fileExtension = result.get('fileExtension') + mimeType = result['mimeType'] + if mimeType == MIMETYPE_GA_FOLDER: + print(f'Skipping download of folder {result["title"]}') + continue + if mimeType in NON_DOWNLOADABLE_MIMETYPES: + print(f'Format of file {result["title"]} not downloadable') + continue + validExtensions = GOOGLEDOC_VALID_EXTENSIONS_MAP.get(mimeType) + if validExtensions: + my_line = 'Downloading Google Doc: %s' + if csvSheetTitle: + my_line += f', Sheet: {csvSheetTitle}' + googleDoc = True + else: + if 'fileSize' in result: + my_line = 'Downloading: %%s of %s bytes' % utils.formatFileSize(int(result['fileSize'])) + else: + my_line = 'Downloading: %s of unknown size' + googleDoc = False + my_line += ' to %s' + csvSheetNotFound = fileDownloaded = fileDownloadFailed = False + for exportFormat in exportFormats: + extension = fileExtension or exportFormat['ext'] + if googleDoc and (extension not in validExtensions): + continue + if targetStdout: + filename = 'stdout' + else: + if targetName: + safe_file_title = targetName + else: + safe_file_title = ''.join(c for c in result['title'] if c in FILENAME_SAFE_CHARS) + if not safe_file_title: + safe_file_title = fileId + filename = os.path.join(targetFolder, safe_file_title) + y = 0 + while True: + if filename.lower()[-len(extension):] != extension.lower(): + filename += extension + if overwrite or not os.path.isfile(filename): + break + y += 1 + filename = os.path.join(targetFolder, f'({y})-{safe_file_title}') + print(my_line % (result['title'], filename)) + spreadsheetUrl = None + if googleDoc: + if csvSheetTitle is None or mimeType != MIMETYPE_GA_SPREADSHEET: + request = drive.files().export_media(fileId=fileId, mimeType=exportFormat['mime']) + if revisionId: + request.uri = f'{request.uri}&revision={revisionId}' + else: + spreadsheet = gapi.call(sheet.spreadsheets(), 'get', + spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))') + for sheet in spreadsheet['sheets']: + if sheet['properties']['title'].lower() == csvSheetTitleLower: + spreadsheetUrl = '{0}?format=csv&id={1}&gid={2}'.format(re.sub('/edit$', '/export', spreadsheet['spreadsheetUrl']), + fileId, sheet['properties']['sheetId']) + break + else: + display.print_error(f'Google Doc: {result["title"]}, Sheet: {csvSheetTitle}, does not exist') + csvSheetNotFound = True + continue + else: + request = drive.files().get_media(fileId=fileId, revisionId=revisionId) + fh = None + try: + fh = open(filename, 'wb') if not targetStdout else sys.stdout + if not spreadsheetUrl: + downloader = googleapiclient.http.MediaIoBaseDownload(fh, request) + done = False + while not done: + status, done = downloader.next_chunk() + if showProgress: + print('Downloaded: {0:>7.2%}'.format(status.progress())) + else: + _, content = drive._http.request(uri=spreadsheetUrl, method='GET') + fh.write(content) + if targetStdout and content[-1] != '\n': + fh.write('\n') + if not targetStdout: + fileutils.close_file(fh) + fileDownloaded = True + break + except (IOError, httplib2.HttpLib2Error) as e: + display.print_error(str(e)) + GM_Globals[GM_SYSEXITRC] = 6 + fileDownloadFailed = True + break + except googleapiclient.http.HttpError as e: + mg = HTTP_ERROR_PATTERN.match(str(e)) + if mg: + display.print_error(mg.group(1)) + else: + display.print_error(str(e)) + fileDownloadFailed = True + break + if fh and not targetStdout: + fileutils.close_file(fh) + os.remove(filename) + if not fileDownloaded and not fileDownloadFailed and not csvSheetNotFound: + display.print_error(f'Format ({",".join(exportFormatChoices)}) not available') + GM_Globals[GM_SYSEXITRC] = 51 + +def showDriveFileInfo(users): + fieldsList = [] + labelsList = [] + fileId = sys.argv[5] + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'allfields': + fieldsList = [] + i += 1 + elif myarg in DRIVEFILE_FIELDS_CHOICES_MAP: + fieldsList.append(DRIVEFILE_FIELDS_CHOICES_MAP[myarg]) + i += 1 + elif myarg in DRIVEFILE_LABEL_CHOICES_MAP: + labelsList.append(DRIVEFILE_LABEL_CHOICES_MAP[myarg]) + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam show fileinfo") + if fieldsList or labelsList: + fieldsList.append('title') + fields = ','.join(set(fieldsList)) + if labelsList: + fields += f',labels({",".join(set(labelsList))})' + else: + fields = '*' + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + feed = gapi.call(drive.files(), 'get', fileId=fileId, fields=fields, supportsAllDrives=True) + if feed: + display.print_json(feed) + +def showDriveFileRevisions(users): + fileId = sys.argv[5] + for user in users: + user, drive = buildDriveGAPIObject(user) + if not drive: + continue + feed = gapi.call(drive.revisions(), 'list', fileId=fileId) + if feed: + display.print_json(feed) + +def transferDriveFiles(users): + target_user = sys.argv[5] + remove_source_user = True + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'keepuser': + remove_source_user = False + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam transfer drive") + target_user, target_drive = buildDriveGAPIObject(target_user) + if not target_drive: + return + target_about = gapi.call(target_drive.about(), 'get', fields='quotaType,quotaBytesTotal,quotaBytesUsed') + if target_about['quotaType'] != 'UNLIMITED': + target_drive_free = int(target_about['quotaBytesTotal']) - int(target_about['quotaBytesUsed']) + else: + target_drive_free = None + for user in users: + user, source_drive = buildDriveGAPIObject(user) + if not source_drive: + continue + counter = 0 + source_about = gapi.call(source_drive.about(), 'get', fields='quotaBytesTotal,quotaBytesUsed,rootFolderId,permissionId') + source_drive_size = int(source_about['quotaBytesUsed']) + if target_drive_free is not None: + if target_drive_free < source_drive_size: + controlflow.system_error_exit(4, MESSAGE_NO_TRANSFER_LACK_OF_DISK_SPACE.format(source_drive_size / 1024 / 1024, target_drive_free / 1024 / 1024)) + print(f'Source drive size: {source_drive_size / 1024 / 1024}mb Target drive free: {target_drive_free / 1024 / 1024}mb') + target_drive_free = target_drive_free - source_drive_size # prep target_drive_free for next user + else: + print(f'Source drive size: {source_drive_size / 1024 / 1024}mb Target drive free: UNLIMITED') + source_root = source_about['rootFolderId'] + source_permissionid = source_about['permissionId'] + print(f'Getting file list for source user: {user}...') + page_message = gapi.got_total_items_msg('Files', '\n') + source_drive_files = gapi.get_all_pages(source_drive.files(), 'list', 'items', page_message=page_message, + q="'me' in owners and trashed = false", fields='items(id,parents,mimeType),nextPageToken') + all_source_file_ids = [] + for source_drive_file in source_drive_files: + all_source_file_ids.append(source_drive_file['id']) + total_count = len(source_drive_files) + print(f'Getting folder list for target user: {target_user}...') + page_message = gapi.got_total_items_msg('Folders', '\n') + target_folders = gapi.get_all_pages(target_drive.files(), 'list', 'items', page_message=page_message, + q="'me' in owners and mimeType = 'application/vnd.google-apps.folder'", fields='items(id,title),nextPageToken') + got_top_folder = False + all_target_folder_ids = [] + for target_folder in target_folders: + all_target_folder_ids.append(target_folder['id']) + if (not got_top_folder) and target_folder['title'] == f'{user} old files': + target_top_folder = target_folder['id'] + got_top_folder = True + if not got_top_folder: + create_folder = gapi.call(target_drive.files(), 'insert', body={'title': f'{user} old files', 'mimeType': 'application/vnd.google-apps.folder'}, fields='id') + target_top_folder = create_folder['id'] + transferred_files = [] + while True: # we loop thru, skipping files until all of their parents are done + skipped_files = False + for drive_file in source_drive_files: + file_id = drive_file['id'] + if file_id in transferred_files: + continue + source_parents = drive_file['parents'] + skip_file_for_now = False + for source_parent in source_parents: + if source_parent['id'] not in all_source_file_ids and source_parent['id'] not in all_target_folder_ids: + continue # means this parent isn't owned by source or target, shouldn't matter + if source_parent['id'] not in transferred_files and source_parent['id'] != source_root: + #print(f'skipping {file_id}') + skipped_files = skip_file_for_now = True + break + if skip_file_for_now: + continue + transferred_files.append(drive_file['id']) + counter += 1 + print(f'Changing owner for file {drive_file["id"]}{currentCount(counter, total_count)}') + body = {'role': 'owner', 'type': 'user', 'value': target_user} + gapi.call(source_drive.permissions(), 'insert', soft_errors=True, fileId=file_id, sendNotificationEmails=False, body=body) + target_parents = [] + for parent in source_parents: + try: + if parent['isRoot']: + target_parents.append({'id': target_top_folder}) + else: + target_parents.append({'id': parent['id']}) + except TypeError: + pass + if not target_parents: + target_parents.append({'id': target_top_folder}) + gapi.call(target_drive.files(), 'patch', soft_errors=True, retry_reasons=[gapi_errors.ErrorReason.NOT_FOUND], fileId=file_id, body={'parents': target_parents}) + if remove_source_user: + gapi.call(target_drive.permissions(), 'delete', soft_errors=True, fileId=file_id, permissionId=source_permissionid) + if not skipped_files: + break + +def sendOrDropEmail(users, method='send'): + body = subject = '' + recipient = labels = sender = None + kwargs = {} + if method in ['insert', 'import']: + kwargs['internalDateSource'] = 'receivedTime' + if method == 'import': + kwargs['neverMarkSpam'] = True + msgHeaders = {} + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'message': + body = sys.argv[i+1] + i += 2 + elif myarg == 'file': + filename = sys.argv[i+1] + i, encoding = getCharSet(i+2) + body = fileutils.read_file(filename, encoding=encoding) + elif myarg == 'subject': + subject = sys.argv[i+1] + i += 2 + elif myarg in ['recipient', 'to']: + recipient = sys.argv[i+1] + i += 2 + elif myarg == 'from': + sender = sys.argv[i+1] + i += 2 + elif myarg == 'header': + msgHeaders[sys.argv[i+1]] = sys.argv[i+2] + i += 3 + elif method in ['insert', 'import'] and myarg == 'labels': + labels = shlexSplitList(sys.argv[i+1]) + i += 2 + elif method in ['insert', 'import'] and myarg == 'deleted': + kwargs['deleted'] = True + i += 1 + elif myarg == 'date': + msgHeaders['Date'] = utils.get_time_or_delta_from_now(sys.argv[i+1]) + if method in ['insert', 'import']: + kwargs['internalDateSource'] = 'dateHeader' + i += 2 + elif method == 'import' and myarg == 'checkspam': + kwargs['neverMarkSpam'] = False + i += 1 + elif method == 'import' and myarg == 'processforcalendar': + kwargs['processForCalendar'] = True + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {method}email") + for user in users: + send_email(subject, body, recipient, sender, user, method, labels, msgHeaders, kwargs) + +def doImap(users): + enable = getBoolean(sys.argv[4], 'gam imap') + body = {'enabled': enable, 'autoExpunge': True, 'expungeBehavior': 'archive', 'maxFolderSize': 0} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'noautoexpunge': + body['autoExpunge'] = False + i += 1 + elif myarg == 'expungebehavior': + opt = sys.argv[i+1].lower() + if opt in EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP: + body['expungeBehavior'] = EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP[opt] + i += 2 + else: + controlflow.expected_argument_exit("gam imap expungebehavior", ", ".join(EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICES_MAP), opt) + elif myarg == 'maxfoldersize': + opt = sys.argv[i+1].lower() + if opt in EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES: + body['maxFolderSize'] = int(opt) + i += 2 + else: + controlflow.expected_argument_exit("gam imap maxfoldersize", "| ".join(EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES), opt) + else: + controlflow.invalid_argument_exit(myarg, "gam imap") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Setting IMAP Access to {str(enable)} for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings(), 'updateImap', + soft_errors=True, + userId='me', body=body) + +def doLanguage(users): + i = 0 + count = len(users) + displayLanguage = sys.argv[4] + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Setting languaged to {displayLanguage} for {user}{currentCount(i, count)}') + result = gapi.call(gmail.users().settings(), 'updateLanguage', userId='me', body={'displayLanguage': displayLanguage}) + print(f'Language is set to {result["displayLanguage"]} for {user}') + +def getLanguage(users): + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings(), 'getLanguage', + soft_errors=True, + userId='me') + if result: + print(f'User: {user}, Language: {result["displayLanguage"]}{currentCount(i, count)}') + +def getImap(users): + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings(), 'getImap', + soft_errors=True, + userId='me') + if result: + enabled = result['enabled'] + if enabled: + print(f'User: {user}, IMAP Enabled: {enabled}, autoExpunge: {result["autoExpunge"]}, expungeBehavior: {result["expungeBehavior"]}, maxFolderSize: {result["maxFolderSize"]}{currentCount(i, count)}') + else: + print(f'User: {user}, IMAP Enabled: {enabled}{currentCount(i, count)}') + +def getProductAndSKU(sku): + l_sku = sku.lower().replace('-', '').replace(' ', '') + for a_sku, sku_values in list(SKUS.items()): + if l_sku == a_sku.lower().replace('-', '') or l_sku in sku_values['aliases'] or l_sku == sku_values['displayName'].lower().replace(' ', ''): + return (sku_values['product'], a_sku) + try: + product = re.search('^([A-Z,a-z]*-[A-Z,a-z]*)', sku).group(1) + except AttributeError: + product = sku + return (product, sku) + +def doLicense(users, operation): + lic = buildGAPIObject('licensing') + sku = sys.argv[5] + productId, skuId = getProductAndSKU(sku) + i = 6 + if len(sys.argv) > 6 and sys.argv[i].lower() in ['product', 'productid']: + productId = sys.argv[i+1] + i += 2 + for user in users: + if operation == 'delete': + print(f'Removing license {_formatSKUIdDisplayName(skuId)} from user {user}') + gapi.call(lic.licenseAssignments(), operation, soft_errors=True, productId=productId, skuId=skuId, userId=user) + elif operation == 'insert': + print(f'Adding license {_formatSKUIdDisplayName(skuId)} to user {user}') + gapi.call(lic.licenseAssignments(), operation, soft_errors=True, productId=productId, skuId=skuId, body={'userId': user}) + elif operation == 'patch': + try: + old_sku = sys.argv[i] + if old_sku.lower() == 'from': + old_sku = sys.argv[i+1] + except KeyError: + controlflow.system_error_exit(2, 'You need to specify the user\'s old SKU as the last argument') + _, old_sku = getProductAndSKU(old_sku) + print(f'Changing user {user} from license {_formatSKUIdDisplayName(old_sku)} to {_formatSKUIdDisplayName(skuId)}') + gapi.call(lic.licenseAssignments(), operation, soft_errors=True, productId=productId, skuId=old_sku, userId=user, body={'skuId': skuId}) + +def doPop(users): + enable = getBoolean(sys.argv[4], 'gam pop') + body = {'accessWindow': ['disabled', 'allMail'][enable], 'disposition': 'leaveInInbox'} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'for': + opt = sys.argv[i+1].lower() + if opt in EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP: + body['accessWindow'] = EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP[opt] + i += 2 + else: + controlflow.expected_argument_exit("gam pop for", ", ".join(EMAILSETTINGS_POP_ENABLE_FOR_CHOICES_MAP), opt) + elif myarg == 'action': + opt = sys.argv[i+1].lower() + if opt in EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP: + body['disposition'] = EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP[opt] + i += 2 + else: + controlflow.expected_argument_exit("gam pop action", ", ".join(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP), opt) + elif myarg == 'confirm': + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam pop") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Setting POP Access to {str(enable)} for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings(), 'updatePop', + soft_errors=True, + userId='me', body=body) + +def getPop(users): + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings(), 'getPop', + soft_errors=True, + userId='me') + if result: + enabled = result['accessWindow'] != 'disabled' + if enabled: + print(f'User: {user}, POP Enabled: {enabled}, For: {result["accessWindow"]}, Action: {result["disposition"]}{currentCount(i, count)}') + else: + print(f'User: {user}, POP Enabled: {enabled}{currentCount(i, count)}') + +SMTPMSA_DISPLAY_FIELDS = ['host', 'port', 'securityMode'] + +def _showSendAs(result, j, jcount, formatSig): + if result['displayName']: + print(f'SendAs Address: {result["displayName"]} <{result["sendAsEmail"]}>{currentCount(j, jcount)}') + else: + print(f'SendAs Address: <{result["sendAsEmail"]}>{currentCount(j, jcount)}') + if result.get('replyToAddress'): + print(f' ReplyTo: {result["replyToAddress"]}') + print(f' IsPrimary: {result.get("isPrimary", False)}') + print(f' Default: {result.get("isDefault", False)}') + if not result.get('isPrimary', False): + print(f' TreatAsAlias: {result.get("treatAsAlias", False)}') + if 'smtpMsa' in result: + for field in SMTPMSA_DISPLAY_FIELDS: + if field in result['smtpMsa']: + print(f' smtpMsa.{field}: {result["smtpMsa"][field]}') + if 'verificationStatus' in result: + print(f' Verification Status: {result["verificationStatus"]}') + sys.stdout.write(' Signature:\n ') + signature = result.get('signature') + if not signature: + signature = 'None' + if formatSig: + print(utils.indentMultiLineText(utils.dehtml(signature), n=4)) + else: + print(utils.indentMultiLineText(signature, n=4)) + +def _processTags(tagReplacements, message): + while True: + match = RT_PATTERN.search(message) + if not match: + break + if tagReplacements.get(match.group(1)): + message = RT_OPEN_PATTERN.sub('', message, count=1) + message = RT_CLOSE_PATTERN.sub('', message, count=1) + else: + message = RT_STRIP_PATTERN.sub('', message, count=1) + while True: + match = RT_TAG_REPLACE_PATTERN.search(message) + if not match: + break + message = re.sub(match.group(0), tagReplacements.get(match.group(1), ''), message) + return message + +def _processSignature(tagReplacements, signature, html): + if signature: + signature = signature.replace('\r', '').replace('\\n', '
') + if tagReplacements: + signature = _processTags(tagReplacements, signature) + if not html: + signature = signature.replace('\n', '
') + return signature + +def getSendAsAttributes(i, myarg, body, tagReplacements, command): + if myarg == 'replace': + matchTag = utils.get_string(i+1, 'Tag') + matchReplacement = utils.get_string(i+2, 'String', minLen=0) + tagReplacements[matchTag] = matchReplacement + i += 3 + elif myarg == 'name': + body['displayName'] = sys.argv[i+1] + i += 2 + elif myarg == 'replyto': + body['replyToAddress'] = sys.argv[i+1] + i += 2 + elif myarg == 'default': + body['isDefault'] = True + i += 1 + elif myarg == 'treatasalias': + body['treatAsAlias'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + else: + controlflow.invalid_argument_exit(myarg, f"gam {command}") + return i + +SMTPMSA_PORTS = ['25', '465', '587'] +SMTPMSA_SECURITY_MODES = ['none', 'ssl', 'starttls'] +SMTPMSA_REQUIRED_FIELDS = ['host', 'port', 'username', 'password'] + +def addUpdateSendAs(users, i, addCmd): + emailAddress = normalizeEmailAddressOrUID(sys.argv[i], noUid=True) + i += 1 + if addCmd: + command = ['sendas', 'add sendas'][i == 6] + body = {'sendAsEmail': emailAddress, 'displayName': sys.argv[i]} + i += 1 + else: + command = 'update sendas' + body = {} + signature = None + smtpMsa = {} + tagReplacements = {} + html = False + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['signature', 'sig']: + signature = sys.argv[i+1] + i += 2 + if signature.lower() == 'file': + filename = sys.argv[i] + i, encoding = getCharSet(i+1) + signature = fileutils.read_file(filename, encoding=encoding) + elif myarg == 'html': + html = True + i += 1 + elif addCmd and myarg.startswith('smtpmsa.'): + if myarg == 'smtpmsa.host': + smtpMsa['host'] = sys.argv[i+1] + i += 2 + elif myarg == 'smtpmsa.port': + value = sys.argv[i+1].lower() + if value not in SMTPMSA_PORTS: + controlflow.expected_argument_exit(myarg, ", ".join(SMTPMSA_PORTS), value) + smtpMsa['port'] = int(value) + i += 2 + elif myarg == 'smtpmsa.username': + smtpMsa['username'] = sys.argv[i+1] + i += 2 + elif myarg == 'smtpmsa.password': + smtpMsa['password'] = sys.argv[i+1] + i += 2 + elif myarg == 'smtpmsa.securitymode': + value = sys.argv[i+1].lower() + if value not in SMTPMSA_SECURITY_MODES: + controlflow.expected_argument_exit(myarg, ", ".join(SMTPMSA_SECURITY_MODES), value) + smtpMsa['securityMode'] = value + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {command}") + else: + i = getSendAsAttributes(i, myarg, body, tagReplacements, command) + if signature is not None: + body['signature'] = _processSignature(tagReplacements, signature, html) + if smtpMsa: + for field in SMTPMSA_REQUIRED_FIELDS: + if field not in smtpMsa: + controlflow.system_error_exit(2, f'smtpmsa.{field} is required.') + body['smtpMsa'] = smtpMsa + kwargs = {'body': body} + if not addCmd: + kwargs['sendAsEmail'] = emailAddress + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Allowing {user} to send as {emailAddress}{currentCount(i, count)}') + gapi.call(gmail.users().settings().sendAs(), ['patch', 'create'][addCmd], + soft_errors=True, + userId='me', **kwargs) + +def deleteSendAs(users): + emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Disallowing {user} to send as {emailAddress}{currentCount(i, count)}') + gapi.call(gmail.users().settings().sendAs(), 'delete', + soft_errors=True, + userId='me', sendAsEmail=emailAddress) + +def updateSmime(users): + smimeIdBase = None + sendAsEmailBase = None + make_default = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'id': + smimeIdBase = sys.argv[i+1] + i += 2 + elif myarg in ['sendas', 'sendasemail']: + sendAsEmailBase = sys.argv[i+1] + i += 2 + elif myarg in ['default']: + make_default = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam update smime") + if not make_default: + print('Nothing to update for smime.') + sys.exit(0) + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + sendAsEmail = sendAsEmailBase if sendAsEmailBase else user + if not smimeIdBase: + result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'list', userId='me', sendAsEmail=sendAsEmail, fields='smimeInfo(id)') + smimes = result.get('smimeInfo', []) + if not smimes: + controlflow.system_error_exit(3, f'{user} has no S/MIME certificates for sendas address {sendAsEmail}') + if len(smimes) > 1: + certList = "\n ".join([smime["id"] for smime in smimes]) + controlflow.system_error_exit(3, f'{user} has more than one S/MIME certificate. Please specify a cert to update:\n {certList}') + smimeId = smimes[0]['id'] + else: + smimeId = smimeIdBase + print(f'Setting smime id {smimeId} as default for user {user} and sendas {sendAsEmail}') + gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'setDefault', userId='me', sendAsEmail=sendAsEmail, id=smimeId) + +def deleteSmime(users): + smimeIdBase = None + sendAsEmailBase = None + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'id': + smimeIdBase = sys.argv[i+1] + i += 2 + elif myarg in ['sendas', 'sendasemail']: + sendAsEmailBase = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(myarg, "gam delete smime") + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + sendAsEmail = sendAsEmailBase if sendAsEmailBase else user + if not smimeIdBase: + result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'list', userId='me', sendAsEmail=sendAsEmail, fields='smimeInfo(id)') + smimes = result.get('smimeInfo', []) + if not smimes: + controlflow.system_error_exit(3, f'{user} has no S/MIME certificates for sendas address {sendAsEmail}') + if len(smimes) > 1: + certList = "\n ".join([smime["id"] for smime in smimes]) + controlflow.system_error_exit(3, f'{user} has more than one S/MIME certificate. Please specify a cert to delete:\n {certList}') + smimeId = smimes[0]['id'] + else: + smimeId = smimeIdBase + print(f'Deleting smime id {smimeId} for user {user} and sendas {sendAsEmail}') + gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'delete', userId='me', sendAsEmail=sendAsEmail, id=smimeId) + +def printShowSmime(users, csvFormat): + if csvFormat: + todrive = False + titles = ['User'] + csvRows = [] + primaryonly = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'primaryonly': + primaryonly = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} smime") + i = 0 + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + if primaryonly: + sendAsEmails = [user] + else: + result = gapi.call(gmail.users().settings().sendAs(), 'list', userId='me', fields='sendAs(sendAsEmail)') + sendAsEmails = [] + for sendAs in result['sendAs']: + sendAsEmails.append(sendAs['sendAsEmail']) + for sendAsEmail in sendAsEmails: + result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'list', sendAsEmail=sendAsEmail, userId='me') + smimes = result.get('smimeInfo', []) + for j, _ in enumerate(smimes): + smimes[j]['expiration'] = utils.formatTimestampYMDHMS(smimes[j]['expiration']) + if csvFormat: + for smime in smimes: + display.add_row_titles_to_csv_file(utils.flatten_json(smime, flattened={'User': user}), csvRows, titles) + else: + display.print_json(smimes) + if csvFormat: + display.write_csv_file(csvRows, titles, 'S/MIME', todrive) + +def printShowSendAs(users, csvFormat): + if csvFormat: + todrive = False + titles = ['User', 'displayName', 'sendAsEmail', 'replyToAddress', 'isPrimary', 'isDefault', 'treatAsAlias', 'verificationStatus'] + csvRows = [] + formatSig = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + elif not csvFormat and myarg == 'format': + formatSig = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} sendas") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings().sendAs(), 'list', + soft_errors=True, + userId='me') + jcount = len(result.get('sendAs', [])) if (result) else 0 + if not csvFormat: + print(f'User: {user}, SendAs Addresses:{currentCount(i, count)}') + if jcount == 0: + continue + j = 0 + for sendas in result['sendAs']: + j += 1 + _showSendAs(sendas, j, jcount, formatSig) + else: + if jcount == 0: + continue + for sendas in result['sendAs']: + row = {'User': user, 'isPrimary': False} + for item in sendas: + if item != 'smtpMsa': + if item not in titles: + titles.append(item) + row[item] = sendas[item] + else: + for field in SMTPMSA_DISPLAY_FIELDS: + if field in sendas[item]: + title = f'smtpMsa.{field}' + if title not in titles: + titles.append(title) + row[title] = sendas[item][field] + csvRows.append(row) + if csvFormat: + display.write_csv_file(csvRows, titles, 'SendAs', todrive) + +def infoSendAs(users): + emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) + formatSig = False + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'format': + formatSig = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam info sendas") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'User: {user}, Show SendAs Address:{currentCount(i, count)}') + result = gapi.call(gmail.users().settings().sendAs(), 'get', + soft_errors=True, + userId='me', sendAsEmail=emailAddress) + if result: + _showSendAs(result, i, count, formatSig) + +def addSmime(users): + sendAsEmailBase = None + setDefault = False + body = {} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'file': + smimefile = sys.argv[i+1] + smimeData = fileutils.read_file(smimefile, mode='rb') + body['pkcs12'] = base64.urlsafe_b64encode(smimeData).decode(UTF8) + i += 2 + elif myarg == 'password': + body['encryptedKeyPassword'] = sys.argv[i+1] + i += 2 + elif myarg == 'default': + setDefault = True + i += 1 + elif myarg in ['sendas', 'sendasemail']: + sendAsEmailBase = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(myarg, "gam add smime") + if 'pkcs12' not in body: + controlflow.system_error_exit(3, 'you must specify a file to upload') + i = 0 + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + sendAsEmail = sendAsEmailBase if sendAsEmailBase else user + result = gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'insert', userId='me', sendAsEmail=sendAsEmail, body=body) + if setDefault: + gapi.call(gmail.users().settings().sendAs().smimeInfo(), 'setDefault', userId='me', sendAsEmail=sendAsEmail, id=result['id']) + print(f'Added S/MIME certificate for user {user} sendas {sendAsEmail} issued by {result["issuerCn"]}') + +def getLabelAttributes(i, myarg, body, function): + if myarg == 'labellistvisibility': + value = sys.argv[i+1].lower().replace('_', '') + if value == 'hide': + body['labelListVisibility'] = 'labelHide' + elif value == 'show': + body['labelListVisibility'] = 'labelShow' + elif value == 'showifunread': + body['labelListVisibility'] = 'labelShowIfUnread' + else: + controlflow.expected_argument_exit("label_list_visibility", ", ".join(["hide", "show", "show_if_unread"]), value) + i += 2 + elif myarg == 'messagelistvisibility': + value = sys.argv[i+1].lower().replace('_', '') + if value not in ['hide', 'show']: + controlflow.expected_argument_exit("message_list_visibility", ", ".join(["hide", "show"]), value) + body['messageListVisibility'] = value + i += 2 + elif myarg == 'backgroundcolor': + body.setdefault('color', {}) + body['color']['backgroundColor'] = getLabelColor(sys.argv[i+1]) + i += 2 + elif myarg == 'textcolor': + body.setdefault('color', {}) + body['color']['textColor'] = getLabelColor(sys.argv[i+1]) + i += 2 + else: + controlflow.invalid_argument_exit(myarg, f"gam {function} labels") + return i + +def checkLabelColor(body): + if 'color' not in body: + return + if 'backgroundColor' in body['color']: + if 'textColor' in body['color']: + return + controlflow.system_error_exit(2, 'textcolor is required.') + controlflow.system_error_exit(2, 'backgroundcolor is required.') + +def doLabel(users, i): + label = sys.argv[i] + i += 1 + body = {'name': label} + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + i = getLabelAttributes(i, myarg, body, "create") + checkLabelColor(body) + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Creating label {label} for {user}{currentCount(i, count)}') + gapi.call(gmail.users().labels(), 'create', soft_errors=True, userId=user, body=body) + +PROCESS_MESSAGE_FUNCTION_TO_ACTION_MAP = {'delete': 'deleted', 'trash': 'trashed', 'untrash': 'untrashed', 'modify': 'modified'} + +def labelsToLabelIds(gmail, labels): + allLabels = { + 'INBOX': 'INBOX', 'SPAM': 'SPAM', 'TRASH': 'TRASH', + 'UNREAD': 'UNREAD', 'STARRED': 'STARRED', 'IMPORTANT': 'IMPORTANT', + 'SENT': 'SENT', 'DRAFT': 'DRAFT', + 'CATEGORY_PERSONAL': 'CATEGORY_PERSONAL', + 'CATEGORY_SOCIAL': 'CATEGORY_SOCIAL', + 'CATEGORY_PROMOTIONS': 'CATEGORY_PROMOTIONS', + 'CATEGORY_UPDATES': 'CATEGORY_UPDATES', + 'CATEGORY_FORUMS': 'CATEGORY_FORUMS', + } + labelIds = list() + for label in labels: + if label not in allLabels: + # first refresh labels in user mailbox + label_results = gapi.call(gmail.users().labels(), 'list', + userId='me', fields='labels(id,name,type)') + for a_label in label_results['labels']: + if a_label['type'] == 'system': + allLabels[a_label['id']] = a_label['id'] + else: + allLabels[a_label['name']] = a_label['id'] + if label not in allLabels: + # if still not there, create it + label_results = gapi.call(gmail.users().labels(), 'create', + body={'labelListVisibility': 'labelShow', + 'messageListVisibility': 'show', 'name': label}, + userId='me', fields='id') + allLabels[label] = label_results['id'] + try: + labelIds.append(allLabels[label]) + except KeyError: + pass + if label.find('/') != -1: + # make sure to create parent labels for proper nesting + parent_label = label[:label.rfind('/')] + while True: + if not parent_label in allLabels: + label_result = gapi.call(gmail.users().labels(), 'create', + userId='me', body={'name': parent_label}) + allLabels[parent_label] = label_result['id'] + if parent_label.find('/') == -1: + break + parent_label = parent_label[:parent_label.rfind('/')] + return labelIds + +def doProcessMessagesOrThreads(users, function, unit='messages'): + query = None + doIt = False + maxToProcess = 1 + body = {} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'query': + query = sys.argv[i+1] + i += 2 + elif myarg == 'doit': + doIt = True + i += 1 + elif myarg in ['maxtodelete', 'maxtotrash', 'maxtomodify', 'maxtountrash']: + maxToProcess = getInteger(sys.argv[i+1], myarg, minVal=0) + i += 2 + elif (function == 'modify') and (myarg == 'addlabel'): + body.setdefault('addLabelIds', []) + body['addLabelIds'].append(sys.argv[i+1]) + i += 2 + elif (function == 'modify') and (myarg == 'removelabel'): + body.setdefault('removeLabelIds', []) + body['removeLabelIds'].append(sys.argv[i+1]) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {function} {unit}") + if not query: + controlflow.system_error_exit(2, 'No query specified. You must specify some query!') + action = PROCESS_MESSAGE_FUNCTION_TO_ACTION_MAP[function] + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Searching {unit.capitalize()} for {user}') + unitmethod = getattr(gmail.users(), unit) + page_message = gapi.got_total_items_msg(f'{unit.capitalize()} for user {user}', '') + listResult = gapi.get_all_pages(unitmethod(), 'list', unit, page_message=page_message, + userId='me', q=query, includeSpamTrash=True, soft_errors=True, fields=f'nextPageToken,{unit}(id)') + result_count = len(listResult) + if not doIt or result_count == 0: + print(f'would try to {function} {result_count} messages for user {user} (max {maxToProcess})\n') + continue + if result_count > maxToProcess: + print(f'WARNING: refusing to {function} ANY messages for user {user} since max messages to process is {maxToProcess} and messages to be {action} is {result_count}\n') + continue + kwargs = {'body': {}} + for my_key in body: + kwargs['body'][my_key] = labelsToLabelIds(gmail, body[my_key]) + i = 0 + if unit == 'messages' and function in ['delete', 'modify']: + batchFunction = f'batch{function.title()}' + id_batches = [[]] + for a_unit in listResult: + id_batches[i].append(a_unit['id']) + if len(id_batches[i]) == 1000: + i += 1 + id_batches.append([]) + processed_messages = 0 + for id_batch in id_batches: + kwargs['body']['ids'] = id_batch + print(f'{function} {len(id_batch)} messages') + gapi.call(unitmethod(), batchFunction, + userId='me', **kwargs) + processed_messages += len(id_batch) + print(f'{function} {processed_messages} of {result_count} messages') + continue + if not kwargs['body']: + del kwargs['body'] + for a_unit in listResult: + i += 1 + print(f' {function} {unit} {a_unit["id"]} for user {user}{currentCount(i, result_count)}') + gapi.call(unitmethod(), function, + id=a_unit['id'], userId='me', **kwargs) + +def doDeleteLabel(users): + label = sys.argv[5] + label_name_lower = label.lower() + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Getting all labels for {user}...') + labels = gapi.call(gmail.users().labels(), 'list', userId=user, fields='labels(id,name,type)') + del_labels = [] + if label == '--ALL_LABELS--': + for del_label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True): + if del_label['type'] != 'system': + del_labels.append(del_label) + elif label[:6].lower() == 'regex:': + regex = label[6:] + p = re.compile(regex) + for del_label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True): + if del_label['type'] != 'system' and p.match(del_label['name']): + del_labels.append(del_label) + else: + for del_label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True): + if label_name_lower == del_label['name'].lower(): + del_labels.append(del_label) + break + else: + print(f' Error: no such label for {user}') + continue + bcount = 0 + i = 0 + count = len(del_labels) + dbatch = gmail.new_batch_http_request(callback=gmail_del_result) + for del_me in del_labels: + i += 1 + print(f' deleting label {del_me["name"]}{currentCount(i, count)}') + dbatch.add(gmail.users().labels().delete(userId=user, id=del_me['id'])) + bcount += 1 + if bcount == 10: + dbatch.execute() + dbatch = gmail.new_batch_http_request(callback=gmail_del_result) + bcount = 0 + if bcount > 0: + dbatch.execute() + +def gmail_del_result(request_id, response, exception): + if exception: + print(exception) + +def showLabels(users): + i = 5 + onlyUser = showCounts = False + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'onlyuser': + onlyUser = True + i += 1 + elif myarg == 'showcounts': + showCounts = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show labels") + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + labels = gapi.call(gmail.users().labels(), 'list', userId=user, soft_errors=True) + if labels: + for label in labels['labels']: + if onlyUser and (label['type'] == 'system'): + continue + print(label['name']) + for a_key in label: + if a_key == 'name': + continue + print(f' {a_key}: {label[a_key]}') + if showCounts: + counts = gapi.call(gmail.users().labels(), 'get', + userId=user, id=label['id'], + fields='messagesTotal,messagesUnread,threadsTotal,threadsUnread') + for a_key in counts: + print(f' {a_key}: {counts[a_key]}') + print('') + +def showGmailProfile(users): + todrive = False + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show gmailprofile") + csvRows = [] + titles = ['emailAddress'] + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + sys.stderr.write(f'Getting Gmail profile for {user}\n') + try: + results = gapi.call(gmail.users(), 'getProfile', + throw_reasons=gapi_errors.GMAIL_THROW_REASONS, + userId='me') + if results: + for item in results: + if item not in titles: + titles.append(item) + csvRows.append(results) + except gapi_errors.GapiServiceNotAvailableError: + entityServiceNotApplicableWarning('User', user, i, count) + display.sort_csv_titles(['emailAddress',], titles) + display.write_csv_file(csvRows, titles, list_type='Gmail Profiles', todrive=todrive) + +def updateLabels(users): + label_name = sys.argv[5] + label_name_lower = label_name.lower() + body = {} + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'name': + body['name'] = sys.argv[i+1] + i += 2 + else: + i = getLabelAttributes(i, myarg, body, "update") + checkLabelColor(body) + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + labels = gapi.call(gmail.users().labels(), 'list', userId=user, fields='labels(id,name)') + for label in labels['labels']: + if label['name'].lower() == label_name_lower: + gapi.call(gmail.users().labels(), 'patch', soft_errors=True, + userId=user, id=label['id'], body=body) + break + else: + print(f'Error: user does not have a label named {label_name}') + +def cleanLabelQuery(labelQuery): + for ch in '/ (){}': + labelQuery = labelQuery.replace(ch, '-') + return labelQuery.lower() + +def renameLabels(users): + search = '^Inbox/(.*)$' + replace = '%s' + merge = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'search': + search = sys.argv[i+1] + i += 2 + elif myarg == 'replace': + replace = sys.argv[i+1] + i += 2 + elif myarg == 'merge': + merge = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam rename label") + pattern = re.compile(search, re.IGNORECASE) + for user in users: + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + labels = gapi.call(gmail.users().labels(), 'list', userId=user) + print(f'got {len(labels["labels"])} labels') + for label in labels['labels']: + if label['type'] == 'system': + continue + match_result = re.search(pattern, label['name']) + if match_result is not None: + try: + new_label_name = replace % match_result.groups() + except TypeError: + controlflow.system_error_exit(2, f'The number of subfields ({len(match_result.groups())}) in search "{search}" does not match the number of subfields ({replace.count("%s")}) in replace "{replace}"') + print(f' Renaming "{label["name"]}" to "{new_label_name}"') + try: + gapi.call(gmail.users().labels(), 'patch', soft_errors=True, throw_reasons=[gapi_errors.ErrorReason.ABORTED], id=label['id'], userId=user, body={'name': new_label_name}) + except gapi_errors.GapiAbortedError: + if merge: + print(f' Merging {label["name"]} label to existing {new_label_name} label') + messages_to_relabel = gapi.get_all_pages(gmail.users().messages(), 'list', 'messages', + userId=user, q=f'label:{cleanLabelQuery(label["name"])}') + if messages_to_relabel: + for new_label in labels['labels']: + if new_label['name'].lower() == new_label_name.lower(): + new_label_id = new_label['id'] + body = {'addLabelIds': [new_label_id]} + break + j = 0 + jcount = len(messages_to_relabel) + for message_to_relabel in messages_to_relabel: + j += 1 + print(f' relabeling message {message_to_relabel["id"]}{currentCount(j, jcount)}') + gapi.call(gmail.users().messages(), 'modify', userId=user, id=message_to_relabel['id'], body=body) + else: + print(f' no messages with {label["name"]} label') + print(f' Deleting label {label["name"]}') + gapi.call(gmail.users().labels(), 'delete', id=label['id'], userId=user) + else: + print(f' Error: looks like {new_label_name} already exists, not renaming. Use the "merge" argument to merge the labels') + +def _getUserGmailLabels(gmail, user, i, count, **kwargs): + try: + labels = gapi.call(gmail.users().labels(), 'list', + throw_reasons=gapi_errors.GMAIL_THROW_REASONS, + userId='me', **kwargs) + if not labels: + labels = {'labels': []} + return labels + except gapi_errors.GapiServiceNotAvailableError: + entityServiceNotApplicableWarning('User', user, i, count) + return None + +def _getLabelId(labels, labelName): + for label in labels['labels']: + if labelName in (label['id'], label['name']): + return label['id'] + return None + +def _getLabelName(labels, labelId): + for label in labels['labels']: + if label['id'] == labelId: + return label['name'] + return labelId + +def _printFilter(user, userFilter, labels): + row = {'User': user, 'id': userFilter['id']} + if 'criteria' in userFilter: + for item in userFilter['criteria']: + if item in ['hasAttachment', 'excludeChats']: + row[item] = item + elif item == 'size': + row[item] = f'size {userFilter["criteria"]["sizeComparison"]} {userFilter["criteria"][item]}' + elif item == 'sizeComparison': + pass + else: + row[item] = f'{item} {userFilter["criteria"][item]}' + else: + row['error'] = 'NoCriteria' + if 'action' in userFilter: + for labelId in userFilter['action'].get('addLabelIds', []): + if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP: + row[FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]] = FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId] + else: + row['label'] = f'label {_getLabelName(labels, labelId)}' + for labelId in userFilter['action'].get('removeLabelIds', []): + if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP: + row[FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]] = FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId] + if userFilter['action'].get('forward'): + row['forward'] = f'forward {userFilter["action"]["forward"]}' + else: + row['error'] = 'NoActions' + return row + +def _showFilter(userFilter, j, jcount, labels): + print(f' Filter: {userFilter["id"]}{currentCount(j, jcount)}') + print(' Criteria:') + if 'criteria' in userFilter: + for item in userFilter['criteria']: + if item in ['hasAttachment', 'excludeChats']: + print(f' {item}') + elif item == 'size': + print(f' {item} {userFilter["criteria"]["sizeComparison"]} {userFilter["criteria"][item]}') + elif item == 'sizeComparison': + pass + else: + print(f' {item} "{userFilter["criteria"][item]}"') + else: + print(' ERROR: No Filter criteria') + print(' Actions:') + if 'action' in userFilter: + for labelId in userFilter['action'].get('addLabelIds', []): + if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP: + print(f' {FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]}') + else: + print(f' label "{_getLabelName(labels, labelId)}"') + for labelId in userFilter['action'].get('removeLabelIds', []): + if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP: + print(f' {FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]}') + if userFilter['action'].get('forward'): + print(f' Forwarding Address: {userFilter["action"]["forward"]}') + else: + print(' ERROR: No Filter actions') + +def addFilter(users, i): + body = {} + addLabelName = None + addLabelIds = [] + removeLabelIds = [] + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in FILTER_CRITERIA_CHOICES_MAP: + myarg = FILTER_CRITERIA_CHOICES_MAP[myarg] + body.setdefault('criteria', {}) + if myarg == 'from': + body['criteria'][myarg] = sys.argv[i+1] + i += 2 + elif myarg == 'to': + body['criteria'][myarg] = sys.argv[i+1] + i += 2 + elif myarg in ['subject', 'query', 'negatedQuery']: + body['criteria'][myarg] = sys.argv[i+1] + i += 2 + elif myarg in ['hasAttachment', 'excludeChats']: + body['criteria'][myarg] = True + i += 1 + elif myarg == 'size': + body['criteria']['sizeComparison'] = sys.argv[i+1].lower() + if body['criteria']['sizeComparison'] not in ['larger', 'smaller']: + controlflow.system_error_exit(2, f'size must be followed by larger or smaller; got {sys.argv[i+1].lower()}') + body['criteria'][myarg] = sys.argv[i+2] + i += 3 + elif myarg in FILTER_ACTION_CHOICES: + body.setdefault('action', {}) + if myarg == 'label': + addLabelName = sys.argv[i+1] + i += 2 + elif myarg == 'important': + addLabelIds.append('IMPORTANT') + if 'IMPORTANT' in removeLabelIds: + removeLabelIds.remove('IMPORTANT') + i += 1 + elif myarg == 'star': + addLabelIds.append('STARRED') + i += 1 + elif myarg == 'trash': + addLabelIds.append('TRASH') + i += 1 + elif myarg == 'notimportant': + removeLabelIds.append('IMPORTANT') + if 'IMPORTANT' in addLabelIds: + addLabelIds.remove('IMPORTANT') + i += 1 + elif myarg == 'markread': + removeLabelIds.append('UNREAD') + i += 1 + elif myarg == 'archive': + removeLabelIds.append('INBOX') + i += 1 + elif myarg == 'neverspam': + removeLabelIds.append('SPAM') + i += 1 + elif myarg == 'forward': + body['action']['forward'] = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam filter") + if 'criteria' not in body: + controlflow.system_error_exit(2, f'you must specify a crtieria <{"|".join(FILTER_CRITERIA_CHOICES_MAP)}> for "gam filter"') + if 'action' not in body: + controlflow.system_error_exit(2, f'you must specify an action <{"|".join(FILTER_ACTION_CHOICES)}> for "gam filter"') + if removeLabelIds: + body['action']['removeLabelIds'] = removeLabelIds + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + labels = _getUserGmailLabels(gmail, user, i, count, fields='labels(id,name)') + if not labels: + continue + if addLabelIds: + body['action']['addLabelIds'] = addLabelIds[:] + if addLabelName: + if not addLabelIds: + body['action']['addLabelIds'] = [] + addLabelId = _getLabelId(labels, addLabelName) + if not addLabelId: + result = gapi.call(gmail.users().labels(), 'create', + soft_errors=True, + userId='me', body={'name': addLabelName}, fields='id') + if not result: + continue + addLabelId = result['id'] + body['action']['addLabelIds'].append(addLabelId) + print(f'Adding filter for {user}{currentCount(i, count)}') + result = gapi.call(gmail.users().settings().filters(), 'create', + soft_errors=True, + userId='me', body=body) + if result: + print(f'User: {user}, Filter: {result["id"]}, Added{currentCount(i, count)}') + +def deleteFilters(users): + filterId = sys.argv[5] + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Deleting filter {filterId} for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings().filters(), 'delete', + soft_errors=True, + userId='me', id=filterId) + +def printShowFilters(users, csvFormat): + if csvFormat: + todrive = False + csvRows = [] + titles = ['User', 'id'] + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} filter") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + labels = gapi.call(gmail.users().labels(), 'list', + soft_errors=True, + userId='me', fields='labels(id,name)') + if not labels: + labels = {'labels': []} + result = gapi.call(gmail.users().settings().filters(), 'list', + soft_errors=True, + userId='me') + jcount = len(result.get('filter', [])) if (result) else 0 + if not csvFormat: + print(f'User: {user}, Filters:{currentCount(i, count)}') + if jcount == 0: + continue + j = 0 + for userFilter in result['filter']: + j += 1 + _showFilter(userFilter, j, jcount, labels) + else: + if jcount == 0: + continue + for userFilter in result['filter']: + row = _printFilter(user, userFilter, labels) + for item in row: + if item not in titles: + titles.append(item) + csvRows.append(row) + if csvFormat: + display.sort_csv_titles(['User', 'id'], titles) + display.write_csv_file(csvRows, titles, 'Filters', todrive) + +def infoFilters(users): + filterId = sys.argv[5] + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + labels = gapi.call(gmail.users().labels(), 'list', + soft_errors=True, + userId='me', fields='labels(id,name)') + if not labels: + labels = {'labels': []} + result = gapi.call(gmail.users().settings().filters(), 'get', + soft_errors=True, + userId='me', id=filterId) + if result: + print(f'User: {user}, Filter:{currentCount(i, count)}') + _showFilter(result, 1, 1, labels) + +def doForward(users): + enable = getBoolean(sys.argv[4], 'gam forward') + body = {'enabled': enable} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP: + body['disposition'] = EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP[myarg] + i += 1 + elif myarg == 'confirm': + i += 1 + elif myarg.find('@') != -1: + body['emailAddress'] = sys.argv[i] + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam forward") + if enable and (not body.get('disposition') or not body.get('emailAddress')): + controlflow.system_error_exit(2, 'you must specify an action and a forwarding address for "gam forward') + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + if enable: + print(f'User: {user}, Forward Enabled: {enable}, Forwarding Address: {body["emailAddress"]}, Action: {body["disposition"]}{currentCount(i, count)}') + else: + print(f'User: {user}, Forward Enabled: {enable}{currentCount(i, count)}') + gapi.call(gmail.users().settings(), 'updateAutoForwarding', + soft_errors=True, + userId='me', body=body) + +def printShowForward(users, csvFormat): + def _showForward(user, i, count, result): + if 'enabled' in result: + enabled = result['enabled'] + if enabled: + print(f'User: {user}, Forward Enabled: {enabled}, Forwarding Address: {result["emailAddress"]}, Action: {result["disposition"]}{currentCount(i, count)}') + else: + print(f'User: {user}, Forward Enabled: {enabled}{currentCount(i, count)}') + else: + enabled = result['enable'] == 'true' + if enabled: + print(f'User: {user}, Forward Enabled: {enabled}, Forwarding Address: {result["forwardTo"]}, Action: {EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result["action"]]}{currentCount(i, count)}') + else: + print(f'User: {user}, Forward Enabled: {enabled}{currentCount(i, count)}') + + def _printForward(user, result): + if 'enabled' in result: + row = {'User': user, 'forwardEnabled': result['enabled']} + if result['enabled']: + row['forwardTo'] = result['emailAddress'] + row['disposition'] = result['disposition'] + else: + row = {'User': user, 'forwardEnabled': result['enable']} + if result['enable'] == 'true': + row['forwardTo'] = result['forwardTo'] + row['disposition'] = EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result['action']] + csvRows.append(row) + + if csvFormat: + todrive = False + csvRows = [] + titles = ['User', 'forwardEnabled', 'forwardTo', 'disposition'] + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} forward") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings(), 'getAutoForwarding', + soft_errors=True, + userId='me') + if result: + if not csvFormat: + _showForward(user, i, count, result) + else: + _printForward(user, result) + if csvFormat: + display.write_csv_file(csvRows, titles, 'Forward', todrive) + +def addForwardingAddresses(users): + emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) + body = {'forwardingEmail': emailAddress} + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Adding Forwarding Address {emailAddress} for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings().forwardingAddresses(), 'create', + soft_errors=True, + userId='me', body=body) + +def deleteForwardingAddresses(users): + emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'deleting Forwarding Address {emailAddress} for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings().forwardingAddresses(), 'delete', + soft_errors=True, + userId='me', forwardingEmail=emailAddress) + +def printShowForwardingAddresses(users, csvFormat): + if csvFormat: + todrive = False + csvRows = [] + titles = ['User', 'forwardingEmail', 'verificationStatus'] + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} forwardingaddresses") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings().forwardingAddresses(), 'list', + soft_errors=True, + userId='me') + jcount = len(result.get('forwardingAddresses', [])) if (result) else 0 + if not csvFormat: + print(f'User: {user}, Forwarding Addresses:{currentCount(i, count)}') + if jcount == 0: + continue + j = 0 + for forward in result['forwardingAddresses']: + j += 1 + print(f' Forwarding Address: {forward["forwardingEmail"]}, Verification Status: {forward["verificationStatus"]}{currentCount(j, jcount)}') + else: + if jcount == 0: + continue + for forward in result['forwardingAddresses']: + row = {'User': user, 'forwardingEmail': forward['forwardingEmail'], 'verificationStatus': forward['verificationStatus']} + csvRows.append(row) + if csvFormat: + display.write_csv_file(csvRows, titles, 'Forwarding Addresses', todrive) + +def infoForwardingAddresses(users): + emailAddress = normalizeEmailAddressOrUID(sys.argv[5], noUid=True) + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + forward = gapi.call(gmail.users().settings().forwardingAddresses(), 'get', + soft_errors=True, + userId='me', forwardingEmail=emailAddress) + if forward: + print(f'User: {user}, Forwarding Address: {forward["forwardingEmail"]}, Verification Status: {forward["verificationStatus"]}{currentCount(i, count)}') + +def doSignature(users): + tagReplacements = {} + i = 4 + if sys.argv[i].lower() == 'file': + filename = sys.argv[i+1] + i, encoding = getCharSet(i+2) + signature = fileutils.read_file(filename, encoding=encoding) + else: + signature = utils.get_string(i, 'String', minLen=0) + i += 1 + body = {} + html = False + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'html': + html = True + i += 1 + else: + i = getSendAsAttributes(i, myarg, body, tagReplacements, 'signature') + body['signature'] = _processSignature(tagReplacements, signature, html) + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Setting Signature for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings().sendAs(), 'patch', + soft_errors=True, + userId='me', body=body, sendAsEmail=user) + +def getSignature(users): + formatSig = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'format': + formatSig = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show signature") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings().sendAs(), 'get', + soft_errors=True, + userId='me', sendAsEmail=user) + if result: + _showSendAs(result, i, count, formatSig) + +def doVacation(users): + enable = getBoolean(sys.argv[4], 'gam vacation') + body = {'enableAutoReply': enable} + if enable: + responseBodyType = 'responseBodyPlainText' + message = None + tagReplacements = {} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'subject': + body['responseSubject'] = sys.argv[i+1] + i += 2 + elif myarg == 'message': + message = sys.argv[i+1] + i += 2 + elif myarg == 'file': + filename = sys.argv[i+1] + i, encoding = getCharSet(i+2) + message = fileutils.read_file(filename, encoding=encoding) + elif myarg == 'replace': + matchTag = utils.get_string(i+1, 'Tag') + matchReplacement = utils.get_string(i+2, 'String', minLen=0) + tagReplacements[matchTag] = matchReplacement + i += 3 + elif myarg == 'html': + responseBodyType = 'responseBodyHtml' + i += 1 + elif myarg == 'contactsonly': + body['restrictToContacts'] = True + i += 1 + elif myarg == 'domainonly': + body['restrictToDomain'] = True + i += 1 + elif myarg == 'startdate': + body['startTime'] = utils.get_yyyymmdd(sys.argv[i+1], returnTimeStamp=True) + i += 2 + elif myarg == 'enddate': + body['endTime'] = utils.get_yyyymmdd(sys.argv[i+1], returnTimeStamp=True) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam vacation") + if message: + if responseBodyType == 'responseBodyHtml': + message = message.replace('\r', '').replace('\\n', '
') + else: + message = message.replace('\r', '').replace('\\n', '\n') + if tagReplacements: + message = _processTags(tagReplacements, message) + body[responseBodyType] = message + if not message and not body.get('responseSubject'): + controlflow.system_error_exit(2, 'You must specify a non-blank subject or message!') + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + print(f'Setting Vacation for {user}{currentCount(i, count)}') + gapi.call(gmail.users().settings(), 'updateVacation', + soft_errors=True, + userId='me', body=body) + +def getVacation(users): + formatReply = False + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'format': + formatReply = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam show vacation") + i = 0 + count = len(users) + for user in users: + i += 1 + user, gmail = buildGmailGAPIObject(user) + if not gmail: + continue + result = gapi.call(gmail.users().settings(), 'getVacation', + soft_errors=True, + userId='me') + if result: + enabled = result['enableAutoReply'] + print(f'User: {user}, Vacation:{currentCount(i, count)}') + print(f' Enabled: {enabled}') + if enabled: + print(f' Contacts Only: {result["restrictToContacts"]}') + print(f' Domain Only: {result["restrictToDomain"]}') + if 'startTime' in result: + print(f' Start Date: {utils.formatTimestampYMD(result["startTime"])}') + else: + print(' Start Date: Started') + if 'endTime' in result: + print(f' End Date: {utils.formatTimestampYMD(result["endTime"])}') + else: + print(' End Date: Not specified') + print(f' Subject: {result.get("responseSubject", "None")}') + sys.stdout.write(' Message:\n ') + if result.get('responseBodyPlainText'): + print(utils.indentMultiLineText(result['responseBodyPlainText'], n=4)) + elif result.get('responseBodyHtml'): + if formatReply: + print(utils.indentMultiLineText(utils.dehtml(result['responseBodyHtml']), n=4)) + else: + print(utils.indentMultiLineText(result['responseBodyHtml'], n=4)) + else: + print('None') + +def doDelSchema(): + cd = buildGAPIObject('directory') + schemaKey = sys.argv[3] + gapi.call(cd.schemas(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaKey) + print(f'Deleted schema {schemaKey}') + +def doCreateOrUpdateUserSchema(updateCmd): + cd = buildGAPIObject('directory') + schemaKey = sys.argv[3] + if updateCmd: + cmd = 'update' + try: + body = gapi.call(cd.schemas(), 'get', throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND], customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaKey) + except gapi_errors.GapiNotFoundError: + controlflow.system_error_exit(3, f'Schema {schemaKey} does not exist.') + else: # create + cmd = 'create' + body = {'schemaName': schemaKey, 'fields': []} + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'field': + if updateCmd: # clear field if it exists on update + for n, field in enumerate(body['fields']): + if field['fieldName'].lower() == sys.argv[i+1].lower(): + del body['fields'][n] + break + a_field = {'fieldName': sys.argv[i+1]} + i += 2 + while True: + myarg = sys.argv[i].lower() + if myarg == 'type': + a_field['fieldType'] = sys.argv[i+1].upper() + validTypes = ['BOOL', 'DOUBLE', 'EMAIL', 'INT64', 'PHONE', 'STRING'] + if a_field['fieldType'] not in validTypes: + controlflow.expected_argument_exit("type", ", ".join(validTypes).lower(), a_field['fieldType']) + i += 2 + elif myarg == 'multivalued': + a_field['multiValued'] = True + i += 1 + elif myarg == 'indexed': + a_field['indexed'] = True + i += 1 + elif myarg == 'restricted': + a_field['readAccessType'] = 'ADMINS_AND_SELF' + i += 1 + elif myarg == 'range': + a_field['numericIndexingSpec'] = {'minValue': getInteger(sys.argv[i+1], myarg), + 'maxValue': getInteger(sys.argv[i+2], myarg)} + i += 3 + elif myarg == 'endfield': + body['fields'].append(a_field) + i += 1 + break + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {cmd} schema") + elif updateCmd and myarg == 'deletefield': + for n, field in enumerate(body['fields']): + if field['fieldName'].lower() == sys.argv[i+1].lower(): + del body['fields'][n] + break + else: + controlflow.system_error_exit(2, f'field {sys.argv[i+1]} not found in schema {schemaKey}') + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {cmd} schema") + if updateCmd: + result = gapi.call(cd.schemas(), 'update', customerId=GC_Values[GC_CUSTOMER_ID], body=body, schemaKey=schemaKey) + print(f'Updated user schema {result["schemaName"]}') + else: + result = gapi.call(cd.schemas(), 'insert', customerId=GC_Values[GC_CUSTOMER_ID], body=body) + print(f'Created user schema {result["schemaName"]}') + +def _showSchema(schema): + print(f'Schema: {schema["schemaName"]}') + for a_key in schema: + if a_key not in ['schemaName', 'fields', 'etag', 'kind']: + print(f' {a_key}: {schema[a_key]}') + for field in schema['fields']: + print(f' Field: {field["fieldName"]}') + for a_key in field: + if a_key not in ['fieldName', 'kind', 'etag']: + print(f' {a_key}: {field[a_key]}') + +def doPrintShowUserSchemas(csvFormat): + cd = buildGAPIObject('directory') + if csvFormat: + todrive = False + csvRows = [] + titles = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} schemas") + schemas = gapi.call(cd.schemas(), 'list', customerId=GC_Values[GC_CUSTOMER_ID]) + if not schemas or 'schemas' not in schemas: + return + for schema in schemas['schemas']: + if not csvFormat: + _showSchema(schema) + else: + row = {'fields.Count': len(schema['fields'])} + display.add_row_titles_to_csv_file(utils.flatten_json(schema, flattened=row), csvRows, titles) + if csvFormat: + display.sort_csv_titles(['schemaId', 'schemaName', 'fields.Count'], titles) + display.write_csv_file(csvRows, titles, 'User Schemas', todrive) + +def doGetUserSchema(): + cd = buildGAPIObject('directory') + schemaKey = sys.argv[3] + schema = gapi.call(cd.schemas(), 'get', customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaKey) + _showSchema(schema) + +def getUserAttributes(i, cd, updateCmd): + def getEntryType(i, entry, entryTypes, setTypeCustom=True, customKeyword='custom', customTypeKeyword='customType'): + """ Get attribute entry type + entryTypes is list of pre-defined types, a|b|c + Allow a|b|c|, a|b|c|custom + setTypeCustom=True, For all fields except organizations, when setting a custom type you do: + entry[u'type'] = u'custom' + entry[u'customType'] = + setTypeCustom=False, For organizations, you don't set entry[u'type'] = u'custom' + Preserve case of custom types + """ + utype = sys.argv[i] + ltype = utype.lower() + if ltype == customKeyword: + i += 1 + utype = sys.argv[i] + ltype = utype.lower() + if ltype in entryTypes: + entry['type'] = ltype + entry.pop(customTypeKeyword, None) + else: + entry[customTypeKeyword] = utype + if setTypeCustom: + entry['type'] = customKeyword + else: + entry.pop('type', None) + return i+1 + + def checkClearBodyList(i, body, itemName): + if sys.argv[i].lower() == 'clear': + body.pop(itemName, None) + body[itemName] = None + return True + return False + + def appendItemToBodyList(body, itemName, itemValue, checkSystemId=False): + if (itemName in body) and (body[itemName] is None): + del body[itemName] + body.setdefault(itemName, []) +# Throw an error if multiple items are marked primary + if itemValue.get('primary', False): + for citem in body[itemName]: + if citem.get('primary', False): + if not checkSystemId or itemValue.get('systemId') == citem.get('systemId'): + controlflow.system_error_exit(2, f'Multiple {itemName} are marked primary, only one can be primary') + body[itemName].append(itemValue) + + def _splitSchemaNameDotFieldName(sn_fn, fnRequired=True): + if sn_fn.find('.') != -1: + schemaName, fieldName = sn_fn.split('.', 1) + schemaName = schemaName.strip() + fieldName = fieldName.strip() + if schemaName and fieldName: + return (schemaName, fieldName) + elif not fnRequired: + schemaName = sn_fn.strip() + if schemaName: + return (schemaName, None) + controlflow.system_error_exit(2, f'{sn_fn} is not a valid custom schema.field name.') + + if updateCmd: + body = {} + need_password = False + else: + body = {'name': {'givenName': 'Unknown', 'familyName': 'Unknown'}} + body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i], noUid=True) + i += 1 + need_password = True + need_to_hash_password = True + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['firstname', 'givenname']: + body.setdefault('name', {}) + body['name']['givenName'] = sys.argv[i+1] + i += 2 + elif myarg in ['lastname', 'familyname']: + body.setdefault('name', {}) + body['name']['familyName'] = sys.argv[i+1] + i += 2 + elif myarg in ['username', 'email', 'primaryemail'] and updateCmd: + body['primaryEmail'] = normalizeEmailAddressOrUID(sys.argv[i+1], noUid=True) + i += 2 + elif myarg == 'customerid' and updateCmd: + body['customerId'] = sys.argv[i+1] + i += 2 + elif myarg == 'password': + need_password = False + body['password'] = sys.argv[i+1] + if body['password'].lower() == 'random': + need_password = True + i += 2 + elif myarg == 'admin': + value = getBoolean(sys.argv[i+1], myarg) + if updateCmd or value: + controlflow.invalid_argument_exit(f"{sys.argv[i]} {value}", f"gam {['create', 'update'][updateCmd]} user") + i += 2 + elif myarg == 'suspended': + body['suspended'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'archived': + body['archived'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'gal': + body['includeInGlobalAddressList'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg in ['sha', 'sha1', 'sha-1']: + body['hashFunction'] = 'SHA-1' + need_to_hash_password = False + i += 1 + elif myarg == 'md5': + body['hashFunction'] = 'MD5' + need_to_hash_password = False + i += 1 + elif myarg == 'crypt': + body['hashFunction'] = 'crypt' + need_to_hash_password = False + i += 1 + elif myarg == 'nohash': + need_to_hash_password = False + i += 1 + elif myarg == 'changepassword': + body['changePasswordAtNextLogin'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'ipwhitelisted': + body['ipWhitelisted'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'agreedtoterms': + body['agreedToTerms'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg in ['org', 'ou']: + body['orgUnitPath'] = getOrgUnitItem(sys.argv[i+1], pathOnly=True) + i += 2 + elif myarg in ['language', 'languages']: + i += 1 + if checkClearBodyList(i, body, 'languages'): + i += 1 + continue + for language in sys.argv[i].replace(',', ' ').split(): + if language.lower() in LANGUAGE_CODES_MAP: + appendItemToBodyList(body, 'languages', {'languageCode': LANGUAGE_CODES_MAP[language.lower()]}) + else: + appendItemToBodyList(body, 'languages', {'customLanguage': language}) + i += 1 + elif myarg == 'gender': + i += 1 + if checkClearBodyList(i, body, 'gender'): + i += 1 + continue + gender = {} + i = getEntryType(i, gender, USER_GENDER_TYPES, customKeyword='other', customTypeKeyword='customGender') + if (i < len(sys.argv)) and (sys.argv[i].lower() == 'addressmeas'): + gender['addressMeAs'] = utils.get_string(i+1, 'String') + i += 2 + body['gender'] = gender + elif myarg in ['address', 'addresses']: + i += 1 + if checkClearBodyList(i, body, 'addresses'): + i += 1 + continue + address = {} + if sys.argv[i].lower() != 'type': + controlflow.system_error_exit(2, f'wrong format for account address details. Expected type got {sys.argv[i]}') + i = getEntryType(i+1, address, USER_ADDRESS_TYPES) + if sys.argv[i].lower() in ['unstructured', 'formatted']: + i += 1 + address['sourceIsStructured'] = False + address['formatted'] = sys.argv[i].replace('\\n', '\n') + i += 1 + while True: + myopt = sys.argv[i].lower() + if myopt == 'pobox': + address['poBox'] = sys.argv[i+1] + i += 2 + elif myopt == 'extendedaddress': + address['extendedAddress'] = sys.argv[i+1] + i += 2 + elif myopt == 'streetaddress': + address['streetAddress'] = sys.argv[i+1] + i += 2 + elif myopt == 'locality': + address['locality'] = sys.argv[i+1] + i += 2 + elif myopt == 'region': + address['region'] = sys.argv[i+1] + i += 2 + elif myopt == 'postalcode': + address['postalCode'] = sys.argv[i+1] + i += 2 + elif myopt == 'country': + address['country'] = sys.argv[i+1] + i += 2 + elif myopt == 'countrycode': + address['countryCode'] = sys.argv[i+1] + i += 2 + elif myopt in ['notprimary', 'primary']: + address['primary'] = myopt == 'primary' + i += 1 + break + else: + controlflow.system_error_exit(2, f'invalid argument ({sys.argv[i]}) for account address details') + appendItemToBodyList(body, 'addresses', address) + elif myarg in ['emails', 'otheremail', 'otheremails']: + i += 1 + if checkClearBodyList(i, body, 'emails'): + i += 1 + continue + an_email = {} + i = getEntryType(i, an_email, USER_EMAIL_TYPES) + an_email['address'] = sys.argv[i] + i += 1 + appendItemToBodyList(body, 'emails', an_email) + elif myarg in ['im', 'ims']: + i += 1 + if checkClearBodyList(i, body, 'ims'): + i += 1 + continue + im = {} + if sys.argv[i].lower() != 'type': + controlflow.system_error_exit(2, f'wrong format for account im details. Expected type got {sys.argv[i]}') + i = getEntryType(i+1, im, USER_IM_TYPES) + if sys.argv[i].lower() != 'protocol': + controlflow.system_error_exit(2, f'wrong format for account details. Expected protocol got {sys.argv[i]}') + i += 1 + im['protocol'] = sys.argv[i].lower() + validProtocols = ['custom_protocol', 'aim', 'gtalk', 'icq', 'jabber', 'msn', 'net_meeting', 'qq', 'skype', 'yahoo'] + if im['protocol'] not in validProtocols: + controlflow.expected_argument_exit("protocol", ", ".join(validProtocols), im['protocol']) + if im['protocol'] == 'custom_protocol': + i += 1 + im['customProtocol'] = sys.argv[i] + i += 1 + # Backwards compatibility: notprimary|primary on either side of IM address + myopt = sys.argv[i].lower() + if myopt in ['notprimary', 'primary']: + im['primary'] = myopt == 'primary' + i += 1 + im['im'] = sys.argv[i] + i += 1 + myopt = sys.argv[i].lower() if i < len(sys.argv) else '' + if myopt in ['notprimary', 'primary']: + im['primary'] = myopt == 'primary' + i += 1 + appendItemToBodyList(body, 'ims', im) + elif myarg in ['organization', 'organizations']: + i += 1 + if checkClearBodyList(i, body, 'organizations'): + i += 1 + continue + organization = {} + while True: + myopt = sys.argv[i].lower() + if myopt == 'name': + organization['name'] = sys.argv[i+1] + i += 2 + elif myopt == 'title': + organization['title'] = sys.argv[i+1] + i += 2 + elif myopt == 'customtype': + organization['customType'] = sys.argv[i+1] + organization.pop('type', None) + i += 2 + elif myopt == 'type': + i = getEntryType(i+1, organization, USER_ORGANIZATION_TYPES, setTypeCustom=False) + elif myopt == 'department': + organization['department'] = sys.argv[i+1] + i += 2 + elif myopt == 'symbol': + organization['symbol'] = sys.argv[i+1] + i += 2 + elif myopt == 'costcenter': + organization['costCenter'] = sys.argv[i+1] + i += 2 + elif myopt == 'location': + organization['location'] = sys.argv[i+1] + i += 2 + elif myopt == 'description': + organization['description'] = sys.argv[i+1] + i += 2 + elif myopt == 'domain': + organization['domain'] = sys.argv[i+1] + i += 2 + elif myopt in ['notprimary', 'primary']: + organization['primary'] = myopt == 'primary' + i += 1 + break + else: + controlflow.system_error_exit(2, f'invalid argument ({sys.argv[i]}) for account organization details') + appendItemToBodyList(body, 'organizations', organization) + elif myarg in ['phone', 'phones']: + i += 1 + if checkClearBodyList(i, body, 'phones'): + i += 1 + continue + phone = {} + while True: + myopt = sys.argv[i].lower() + if myopt == 'value': + phone['value'] = sys.argv[i+1] + i += 2 + elif myopt == 'type': + i = getEntryType(i+1, phone, USER_PHONE_TYPES) + elif myopt in ['notprimary', 'primary']: + phone['primary'] = myopt == 'primary' + i += 1 + break + else: + controlflow.system_error_exit(2, f'invalid argument ({sys.argv[i]}) for account phone details') + appendItemToBodyList(body, 'phones', phone) + elif myarg in ['relation', 'relations']: + i += 1 + if checkClearBodyList(i, body, 'relations'): + i += 1 + continue + relation = {} + i = getEntryType(i, relation, USER_RELATION_TYPES) + relation['value'] = sys.argv[i] + i += 1 + appendItemToBodyList(body, 'relations', relation) + elif myarg in ['externalid', 'externalids']: + i += 1 + if checkClearBodyList(i, body, 'externalIds'): + i += 1 + continue + externalid = {} + i = getEntryType(i, externalid, USER_EXTERNALID_TYPES) + externalid['value'] = sys.argv[i] + i += 1 + appendItemToBodyList(body, 'externalIds', externalid) + elif myarg in ['website', 'websites']: + i += 1 + if checkClearBodyList(i, body, 'websites'): + i += 1 + continue + website = {} + i = getEntryType(i, website, USER_WEBSITE_TYPES) + website['value'] = sys.argv[i] + i += 1 + myopt = sys.argv[i].lower() if i < len(sys.argv) else '' + if myopt in ['notprimary', 'primary']: + website['primary'] = myopt == 'primary' + i += 1 + appendItemToBodyList(body, 'websites', website) + elif myarg in ['note', 'notes']: + i += 1 + if checkClearBodyList(i, body, 'notes'): + i += 1 + continue + note = {} + if sys.argv[i].lower() in ['text_plain', 'text_html']: + note['contentType'] = sys.argv[i].lower() + i += 1 + if sys.argv[i].lower() == 'file': + filename = sys.argv[i+1] + i, encoding = getCharSet(i+2) + note['value'] = fileutils.read_file(filename, encoding=encoding) + else: + note['value'] = sys.argv[i].replace('\\n', '\n') + i += 1 + body['notes'] = note + elif myarg in ['location', 'locations']: + i += 1 + if checkClearBodyList(i, body, 'locations'): + i += 1 + continue + location = {'type': 'desk', 'area': ''} + while True: + myopt = sys.argv[i].lower() + if myopt == 'type': + i = getEntryType(i+1, location, USER_LOCATION_TYPES) + elif myopt == 'area': + location['area'] = sys.argv[i+1] + i += 2 + elif myopt in ['building', 'buildingid']: + location['buildingId'] = gapi_directory_resource.getBuildingByNameOrId(cd, sys.argv[i+1]) + i += 2 + elif myopt in ['desk', 'deskcode']: + location['deskCode'] = sys.argv[i+1] + i += 2 + elif myopt in ['floor', 'floorname']: + location['floorName'] = sys.argv[i+1] + i += 2 + elif myopt in ['section', 'floorsection']: + location['floorSection'] = sys.argv[i+1] + i += 2 + elif myopt in ['endlocation']: + i += 1 + break + else: + controlflow.system_error_exit(3, f'{myopt} is not a valid argument for user location details. Make sure user location details end with an endlocation argument') + appendItemToBodyList(body, 'locations', location) + elif myarg in ['ssh', 'sshkeys', 'sshpublickeys']: + i += 1 + if checkClearBodyList(i, body, 'sshPublicKeys'): + i += 1 + continue + ssh = {} + while True: + myopt = sys.argv[i].lower() + if myopt == 'expires': + ssh['expirationTimeUsec'] = getInteger(sys.argv[i+1], myopt, minVal=0) + i += 2 + elif myopt == 'key': + ssh['key'] = sys.argv[i+1] + i += 2 + elif myopt in ['endssh']: + i += 1 + break + else: + controlflow.system_error_exit(3, f'{myopt} is not a valid argument for user ssh details. Make sure user ssh details end with an endssh argument') + appendItemToBodyList(body, 'sshPublicKeys', ssh) + elif myarg in ['posix', 'posixaccounts']: + i += 1 + if checkClearBodyList(i, body, 'posixAccounts'): + i += 1 + continue + posix = {} + while True: + myopt = sys.argv[i].lower() + if myopt == 'gecos': + posix['gecos'] = sys.argv[i+1] + i += 2 + elif myopt == 'gid': + posix['gid'] = getInteger(sys.argv[i+1], myopt, minVal=0) + i += 2 + elif myopt == 'uid': + posix['uid'] = getInteger(sys.argv[i+1], myopt, minVal=1000) + i += 2 + elif myopt in ['home', 'homedirectory']: + posix['homeDirectory'] = sys.argv[i+1] + i += 2 + elif myopt in ['primary']: + posix['primary'] = getBoolean(sys.argv[i+1], myopt) + i += 2 + elif myopt in ['shell']: + posix['shell'] = sys.argv[i+1] + i += 2 + elif myopt in ['system', 'systemid']: + posix['systemId'] = sys.argv[i+1] + i += 2 + elif myopt in ['username', 'name']: + posix['username'] = sys.argv[i+1] + i += 2 + elif myopt in ['os', 'operatingsystemtype']: + posix['operatingSystemType'] = sys.argv[i+1] + i += 2 + elif myopt in ['endposix']: + i += 1 + break + else: + controlflow.system_error_exit(3, f'{myopt} is not a valid argument for user posix details. Make sure user posix details end with an endposix argument') + appendItemToBodyList(body, 'posixAccounts', posix, checkSystemId=True) + elif myarg in ['keyword', 'keywords']: + i += 1 + if checkClearBodyList(i, body, 'keywords'): + i += 1 + continue + keyword = {} + i = getEntryType(i, keyword, USER_KEYWORD_TYPES) + keyword['value'] = sys.argv[i] + i += 1 + appendItemToBodyList(body, 'keywords', keyword) + elif myarg in ['recoveryemail']: + body['recoveryEmail'] = sys.argv[i+1] + i += 2 + elif myarg in ['recoveryphone']: + body['recoveryPhone'] = sys.argv[i+1] + if body['recoveryPhone'] and body['recoveryPhone'][0] != '+': + body['recoveryPhone'] = '+' + body['recoveryPhone'] + i += 2 + elif myarg == 'clearschema': + if not updateCmd: + controlflow.invalid_argument_exit(sys.argv[i], "gam create user") + schemaName, fieldName = _splitSchemaNameDotFieldName(sys.argv[i+1], False) + up = 'customSchemas' + body.setdefault(up, {}) + body[up].setdefault(schemaName, {}) + if fieldName is None: + schema = gapi.call(cd.schemas(), 'get', + soft_errors=True, + customerId=GC_Values[GC_CUSTOMER_ID], schemaKey=schemaName, fields='fields(fieldName)') + if not schema: + sys.exit(2) + for field in schema['fields']: + body[up][schemaName][field['fieldName']] = None + else: + body[up][schemaName][fieldName] = None + i += 2 + elif myarg.find('.') >= 0: + schemaName, fieldName = _splitSchemaNameDotFieldName(sys.argv[i]) + up = 'customSchemas' + body.setdefault(up, {}) + body[up].setdefault(schemaName, {}) + i += 1 + multivalue = sys.argv[i].lower() + if multivalue in ['multivalue', 'multivalued', 'value', 'multinonempty']: + i += 1 + body[up][schemaName].setdefault(fieldName, []) + schemaValue = {} + if sys.argv[i].lower() == 'type': + i += 1 + schemaValue['type'] = sys.argv[i].lower() + validSchemaTypes = ['custom', 'home', 'other', 'work'] + if schemaValue['type'] not in validSchemaTypes: + controlflow.expected_argument_exit("schema type", ", ".join(validSchemaTypes), schemaValue['type']) + i += 1 + if schemaValue['type'] == 'custom': + schemaValue['customType'] = sys.argv[i] + i += 1 + schemaValue['value'] = sys.argv[i] + if schemaValue['value'] or multivalue != 'multinonempty': + body[up][schemaName][fieldName].append(schemaValue) + else: + body[up][schemaName][fieldName] = sys.argv[i] + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], f"gam {['create', 'update'][updateCmd]} user") + if need_password: + rnd = SystemRandom() + body['password'] = ''.join(rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(100)) + if 'password' in body and need_to_hash_password: + body['password'] = gen_sha512_hash(body['password']) + body['hashFunction'] = 'crypt' + return body + +def getCRMService(login_hint): + scopes = ['https://www.googleapis.com/auth/cloud-platform'] + client_id = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com' + client_secret = 'qM3dP8f_4qedwzWQE1VR4zzU' + creds = gam.auth.oauth.Credentials.from_client_secrets( + client_id, client_secret, + scopes, 'online', + login_hint=login_hint, + use_console_flow=not GC_Values[GC_OAUTH_BROWSER]) + httpc = transport.AuthorizedHttp(creds) + return getService('cloudresourcemanagerv1', httpc), httpc + +# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here. +def getCRM2Service(httpc): + return getService('cloudresourcemanager', httpc) + +def getGAMProjectFile(filepath): + # if file exists locally in GAM path then use it. + # allows for testing changes before updating project. + local_file = os.path.join(GM_Globals[GM_GAM_PATH], filepath) + if os.path.isfile(local_file): + return fileutils.read_file(local_file, continue_on_error=False, display_errors=True) + file_url = GAM_PROJECT_FILEPATH+filepath + httpObj = transport.create_http() + _, c = httpObj.request(file_url, 'GET') + return c.decode(UTF8) + +def enableGAMProjectAPIs(GAMProjectAPIs, httpObj, projectId, checkEnabled, i=0, count=0): + apis = GAMProjectAPIs[:] + project_name = f'project:{projectId}' + serveman = getService('servicemanagement', httpObj) + status = True + if checkEnabled: + try: + services = gapi.get_all_pages(serveman.services(), 'list', 'services', + throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND], + consumerId=project_name, fields='nextPageToken,services(serviceName)') + jcount = len(services) + print(f' Project: {projectId}, Check {jcount} APIs{currentCount(i, count)}') + j = 0 + for service in sorted(services, key=lambda k: k['serviceName']): + j += 1 + if 'serviceName' in service: + if service['serviceName'] in apis: + print(f' API: {service["serviceName"]}, Already enabled{currentCount(j, jcount)}') + apis.remove(service['serviceName']) + else: + print(f' API: {service["serviceName"]}, Already enabled (non-GAM which is fine){currentCount(j, jcount)}') + except gapi_errors.GapiNotFoundError as e: + print(f' Project: {projectId}, Update Failed: {str(e)}{currentCount(i, count)}') + status = False + jcount = len(apis) + if status and jcount > 0: + print(f' Project: {projectId}, Enable {jcount} APIs{currentCount(i, count)}') + j = 0 + for api in apis: + j += 1 + while True: + try: + gapi.call(serveman.services(), 'enable', + throw_reasons=[gapi_errors.ErrorReason.FAILED_PRECONDITION, gapi_errors.ErrorReason.FORBIDDEN, gapi_errors.ErrorReason.PERMISSION_DENIED], + serviceName=api, body={'consumerId': project_name}) + print(f' API: {api}, Enabled{currentCount(j, jcount)}') + break + except gapi_errors.GapiFailedPreconditionError as e: + print(f'\nThere was an error enabling {api}. Please resolve error as described below:') + print() + print(f'\n{str(e)}\n') + print() + input('Press enter once resolved and we will try enabling the API again.') + except (gapi_errors.GapiForbiddenError, gapi_errors.GapiPermissionDeniedError) as e: + print(f' API: {api}, Enable Failed: {str(e)}{currentCount(j, jcount)}') + status = False + return status + +def _grantSARotateRights(iam, sa_email): + print(f'Giving service account {sa_email} rights to rotate own private key') + body = { + 'policy': { + 'bindings': [ + { + 'role': 'roles/iam.serviceAccountKeyAdmin', + 'members': [f'serviceAccount:{sa_email}'] + } + ] + } + } + gapi.call(iam.projects().serviceAccounts(), 'setIamPolicy', resource=f'projects/-/serviceAccounts/{sa_email}', + body=body) + +def setGAMProjectConsentScreen(httpObj, projectId, login_hint): + print('Setting GAM project consent screen...') + iap = getService('iap', httpObj) + body = {'applicationTitle': 'GAM', 'supportEmail': login_hint} + gapi.call(iap.projects().brands(), 'create', + parent=f'projects/{projectId}', body=body) + +def _createClientSecretsOauth2service(httpObj, projectId, login_hint, create_project): + + def _checkClientAndSecret(simplehttp, client_id, client_secret): + url = 'https://oauth2.googleapis.com/token' + post_data = {'client_id': client_id, 'client_secret': client_secret, + 'code': 'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid', + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', 'grant_type': 'authorization_code'} + headers = {'Content-type': 'application/x-www-form-urlencoded'} + _, content = simplehttp.request(url, 'POST', urlencode(post_data), headers=headers) + try: + content = json.loads(content) + except ValueError: + print(f'Unknown error: {content}') + return False + if not 'error' in content or not 'error_description' in content: + print(f'Unknown error: {content}') + return False + if content['error'] == 'invalid_grant': + return True + if content['error_description'] == 'The OAuth client was not found.': + print(f'Ooops!!\n\n{client_id}\n\nIs not a valid client ID. Please make sure you are following the directions exactly and that there are no extra spaces in your client ID.') + return False + if content['error_description'] == 'Unauthorized': + print(f'Ooops!!\n\n{client_secret}\n\nIs not a valid client secret. Please make sure you are following the directions exactly and that there are no extra spaces in your client secret.') + return False + print(f'Unknown error: {content}') + return False + + GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines() + enableGAMProjectAPIs(GAMProjectAPIs, httpObj, projectId, False) + if create_project: + setGAMProjectConsentScreen(httpObj, projectId, login_hint) + iam = getService('iam', httpObj) + sa_list = gapi.call(iam.projects().serviceAccounts(), 'list', + name=f'projects/{projectId}') + service_account = None + if 'accounts' in sa_list: + for account in sa_list['accounts']: + sa_email = f'{projectId}@{projectId}.iam.gserviceaccount.com' + if sa_email in account['name']: + service_account = account + break + if not service_account: + print('Creating Service Account') + service_account = gapi.call(iam.projects().serviceAccounts(), 'create', + name=f'projects/{projectId}', + body={'accountId': projectId, 'serviceAccount': {'displayName': 'GAM Project'}}) + GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = service_account['uniqueId'] + doCreateOrRotateServiceAccountKeys(iam, project_id=service_account['projectId'], + client_email=service_account['email'], + client_id=service_account['uniqueId']) + _grantSARotateRights(iam, service_account['name'].rsplit('/', 1)[-1]) + console_url = f'https://console.cloud.google.com/apis/credentials/oauthclient?project={projectId}' + while True: + print(f'''Please go to: + +{console_url} + +1. Choose "Desktop App" or "Other" for "Application type". +2. Enter a desired value for "Name" or leave as is. +3. Click the blue "Create" button. +4. Copy the "client ID" value that shows on the next page. + +''') +# If you use Firefox to copy the Client ID and Secret, the data has leading and trailing newlines +# The first raw_input will get the leading newline, thus we have to issue another raw_input to get the data +# If the newlines are not present, the data is correctly read with the first raw_input + client_id = input('Enter your Client ID: ').strip() + if not client_id: + client_id = input().strip() + print('\nNow go back to your browser and copy your client secret.') + client_secret = input('Enter your Client Secret: ').strip() + if not client_secret: + client_secret = input().strip() + simplehttp = transport.create_http() + client_valid = _checkClientAndSecret(simplehttp, client_id, client_secret) + if client_valid: + break + print() + cs_data = f'''{{ + "installed": {{ + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth", + "client_id": "{client_id}", + "client_secret": "{client_secret}", + "created_by": "{login_hint}", + "project_id": "{projectId}", + "redirect_uris": ["http://localhost", "urn:ietf:wg:oauth:2.0:oob"], + "token_uri": "https://oauth2.googleapis.com/token" + }} +}}''' + fileutils.write_file(GC_Values[GC_CLIENT_SECRETS_JSON], cs_data, continue_on_error=False) + print('That\'s it! Your GAM Project is created and ready to use.') + +VALIDEMAIL_PATTERN = re.compile(r'^[^@]+@[^@]+\.[^@]+$') + +def _getValidateLoginHint(login_hint=None): + while True: + if not login_hint: + login_hint = input('\nWhat is your G Suite admin email address? ').strip() + if login_hint.find('@') == -1 and GC_Values[GC_DOMAIN]: + login_hint = f'{login_hint}@{GC_Values[GC_DOMAIN].lower()}' + if VALIDEMAIL_PATTERN.match(login_hint): + return login_hint + print(f'{ERROR_PREFIX}Invalid email address: {login_hint}') + login_hint = None + +def _getCurrentProjectID(): + cs_data = fileutils.read_file(GC_Values[GC_CLIENT_SECRETS_JSON], continue_on_error=True, display_errors=True) + if not cs_data: + controlflow.system_error_exit(14, f'Your client secrets file:\n\n{GC_Values[GC_CLIENT_SECRETS_JSON]}\n\nis missing. Please recreate the file.') + try: + return json.loads(cs_data)['installed']['project_id'] + except (ValueError, IndexError, KeyError): + controlflow.system_error_exit(3, f'The format of your client secrets file:\n\n{GC_Values[GC_CLIENT_SECRETS_JSON]}\n\nis incorrect. Please recreate the file.') + +def _getProjects(crm, pfilter): + try: + return gapi.get_all_pages(crm.projects(), 'list', 'projects', throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST], filter=pfilter) + except gapi_errors.GapiBadRequestError as e: + controlflow.system_error_exit(2, f'Project: {pfilter}, {str(e)}') + +PROJECTID_PATTERN = re.compile(r'^[a-z][a-z0-9-]{4,28}[a-z0-9]$') +PROJECTID_FORMAT_REQUIRED = '[a-z][a-z0-9-]{4,28}[a-z0-9]' + +def _getLoginHintProjectId(createCmd): + login_hint = None + projectId = None + parent = None + if len(sys.argv) >= 4 and sys.argv[3].lower() not in ['admin', 'project', 'parent']: + # legacy "gam create/use project + try: + login_hint = sys.argv[3] + projectId = sys.argv[4] + except IndexError: + pass + else: + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'admin': + login_hint = sys.argv[i+1] + i += 2 + elif myarg == 'project': + projectId = sys.argv[i+1] + i += 2 + elif createCmd and myarg == 'parent': + parent = sys.argv[i+1] + i += 2 + else: + expected = ['admin', 'project'] + if createCmd: + expected.append('parent') + controlflow.system_error_exit(3, f'{myarg} is not a valid argument for "gam {["use", "create"][createCmd]} project", expected one of: {", ".join(expected)}') + login_hint = _getValidateLoginHint(login_hint) + if projectId: + if not PROJECTID_PATTERN.match(projectId): + controlflow.system_error_exit(2, f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>') + elif createCmd: + projectId = 'gam-project' + for _ in range(3): + projectId += f'-{"".join(random.choice(LOWERNUMERIC_CHARS) for _ in range(3))}' + else: + projectId = input('\nWhat is your API project ID? ').strip() + if not PROJECTID_PATTERN.match(projectId): + controlflow.system_error_exit(2, f'Invalid Project ID: {projectId}, expected <{PROJECTID_FORMAT_REQUIRED}>') + crm, httpObj = getCRMService(login_hint) + if parent and not parent.startswith('organizations/') and not parent.startswith('folders/'): + crm2 = getCRM2Service(httpObj) + parent = convertGCPFolderNameToID(parent, crm2) + if parent: + parent_type, parent_id = parent.split('/') + if parent_type[-1] == 's': + parent_type = parent_type[:-1] # folders > folder, organizations > organization + parent = {'type': parent_type, 'id': parent_id} + projects = _getProjects(crm, f'id:{projectId}') + if not createCmd: + if not projects: + controlflow.system_error_exit(2, f'User: {login_hint}, Project ID: {projectId}, Does not exist') + if projects[0]['lifecycleState'] != 'ACTIVE': + controlflow.system_error_exit(2, f'User: {login_hint}, Project ID: {projectId}, Not active') + else: + if projects: + controlflow.system_error_exit(2, f'User: {login_hint}, Project ID: {projectId}, Duplicate') + return (crm, httpObj, login_hint, projectId, parent) + +PROJECTID_FILTER_REQUIRED = 'gam||(filter )' +def convertGCPFolderNameToID(parent, crm2): + # crm2.folders() is broken requiring pageToken, etc in body, not URL. + # for now just use gapi.get_items and if user has that many folders they'll + # just need to be specific. + folders = gapi.get_items(crm2.folders(), 'search', items='folders', + body={'pageSize': 1000, 'query': f'displayName="{parent}"'}) + if not folders: + controlflow.system_error_exit(1, f'ERROR: No folder found matching displayName={parent}') + if len(folders) > 1: + print('Multiple matches:') + for folder in folders: + print(f' Name: {folder["name"]} ID: {folder["displayName"]}') + controlflow.system_error_exit(2, 'ERROR: Multiple matching folders, please specify one.') + return folders[0]['name'] + +def createGCPFolder(): + login_hint = _getValidateLoginHint() + _, httpObj = getCRMService(login_hint) + crm2 = getCRM2Service(httpObj) + gapi.call(crm2.folders(), 'create', body={'name': sys.argv[3], 'displayName': sys.argv[3]}) + +def _getLoginHintProjects(printShowCmd): + login_hint = None + pfilter = None + i = 3 + try: + login_hint = sys.argv[i] + i += 1 + pfilter = sys.argv[i] + i += 1 + except IndexError: + pass + if not pfilter: + pfilter = 'current' if not printShowCmd else 'id:gam-project-*' + elif printShowCmd and pfilter.lower() == 'all': + pfilter = None + elif pfilter.lower() == 'gam': + pfilter = 'id:gam-project-*' + elif pfilter.lower() == 'filter': + pfilter = sys.argv[i] + i += 1 + elif PROJECTID_PATTERN.match(pfilter): + pfilter = f'id:{pfilter}' + else: + controlflow.system_error_exit(2, f'Invalid Project ID: {pfilter}, expected <{["", "all|"][printShowCmd]}{PROJECTID_FILTER_REQUIRED}>') + login_hint = _getValidateLoginHint(login_hint) + crm, httpObj = getCRMService(login_hint) + if pfilter in ['current', 'id:current']: + projectID = _getCurrentProjectID() + if not printShowCmd: + projects = [{'projectId': projectID}] + else: + projects = _getProjects(crm, f'id:{projectID}') + else: + projects = _getProjects(crm, pfilter) + return (crm, httpObj, login_hint, projects, i) + +def _checkForExistingProjectFiles(): + for a_file in [GC_Values[GC_OAUTH2SERVICE_JSON], GC_Values[GC_CLIENT_SECRETS_JSON]]: + if os.path.exists(a_file): + controlflow.system_error_exit(5, f'{a_file} already exists. Please delete or rename it before attempting to use another project.') + +def doCreateProject(): + _checkForExistingProjectFiles() + crm, httpObj, login_hint, projectId, parent = _getLoginHintProjectId(True) + login_domain = login_hint[login_hint.find('@')+1:] + body = {'projectId': projectId, 'name': 'GAM Project'} + if parent: + body['parent'] = parent + while True: + create_again = False + print(f'Creating project "{body["name"]}"...') + create_operation = gapi.call(crm.projects(), 'create', body=body) + operation_name = create_operation['name'] + time.sleep(8) # Google recommends always waiting at least 5 seconds + for i in range(1, 5): + print('Checking project status...') + status = gapi.call(crm.operations(), 'get', + name=operation_name) + if 'error' in status: + if status['error'].get('message', '') == 'No permission to create project in organization': + print('Hmm... Looks like you have no rights to your Google Cloud Organization.') + print('Attempting to fix that...') + getorg = gapi.call(crm.organizations(), 'search', + body={'filter': f'domain:{login_domain}'}) + try: + organization = getorg['organizations'][0]['name'] + print(f'Your organization name is {organization}') + except (KeyError, IndexError): + controlflow.system_error_exit(3, 'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.') + org_policy = gapi.call(crm.organizations(), 'getIamPolicy', + resource=organization) + if 'bindings' not in org_policy: + org_policy['bindings'] = [] + print('Looks like no one has rights to your Google Cloud Organization. Attempting to give you create rights...') + else: + print('The following rights seem to exist:') + for a_policy in org_policy['bindings']: + if 'role' in a_policy: + print(f' Role: {a_policy["role"]}') + if 'members' in a_policy: + print(' Members:') + for member in a_policy['members']: + print(f' {member}') + print() + my_role = 'roles/resourcemanager.projectCreator' + print(f'Giving {login_hint} the role of {my_role}...') + org_policy['bindings'].append({'role': my_role, 'members': [f'user:{login_hint}']}) + gapi.call(crm.organizations(), 'setIamPolicy', + resource=organization, body={'policy': org_policy}) + create_again = True + break + try: + if status['error']['details'][0]['violations'][0]['description'] == 'Callers must accept Terms of Service': + print('''Please go to: + +https://console.cloud.google.com/start + +and accept the Terms of Service (ToS). As soon as you've accepted the ToS popup, you can return here and press enter.''') + input() + create_again = True + break + except (IndexError, KeyError): + pass + controlflow.system_error_exit(1, status) + if status.get('done', False): + break + sleep_time = i ** 2 + print(f'Project still being created. Sleeping {sleep_time} seconds') + time.sleep(sleep_time) + if create_again: + continue + if not status.get('done', False): + controlflow.system_error_exit(1, f'Failed to create project: {status}') + elif 'error' in status: + controlflow.system_error_exit(2, status['error']) + break + _createClientSecretsOauth2service(httpObj, projectId, login_hint, True) + +def doUseProject(): + _checkForExistingProjectFiles() + _, httpObj, login_hint, projectId, _ = _getLoginHintProjectId(False) + _createClientSecretsOauth2service(httpObj, projectId, login_hint, False) + +def doUpdateProjects(): + _, httpObj, login_hint, projects, _ = _getLoginHintProjects(False) + GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines() + count = len(projects) + print(f'User: {login_hint}, Update {count} Projects') + i = 0 + for project in projects: + i += 1 + projectId = project['projectId'] + enableGAMProjectAPIs(GAMProjectAPIs, httpObj, projectId, True, i, count) + iam = getService('iam', httpObj) + _getSvcAcctData() # needed to read in GM_OAUTH2SERVICE_JSON_DATA + sa_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email'] + _grantSARotateRights(iam, sa_email) + +def _generatePrivateKeyAndPublicCert(client_id, key_size): + print(' Generating new private key...') + private_key = rsa.generate_private_key(public_exponent=65537, + key_size=key_size, backend=default_backend()) + private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption()).decode() + print(' Extracting public certificate...') + public_key = private_key.public_key() + builder = x509.CertificateBuilder() + builder = builder.subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, client_id)])) + builder = builder.issuer_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, client_id)])) + not_valid_before = datetime.datetime.today() - datetime.timedelta(days=1) + not_valid_after = datetime.datetime.today() + datetime.timedelta(days=365*10-1) + builder = builder.not_valid_before(not_valid_before) + builder = builder.not_valid_after(not_valid_after) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.public_key(public_key) + builder = builder.add_extension(x509.BasicConstraints(ca=False, + path_length=None), critical=True) + builder = builder.add_extension(x509.KeyUsage(key_cert_sign=False, + crl_sign=False, digital_signature=True, content_commitment=False, + key_encipherment=False, data_encipherment=False, key_agreement=False, + encipher_only=False, decipher_only=False), critical=True) + builder = builder.add_extension( + x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), + critical=True) + certificate = builder.sign(private_key=private_key, + algorithm=hashes.SHA256(), backend=default_backend()) + public_cert_pem = certificate.public_bytes(serialization.Encoding.PEM).decode() + publicKeyData = base64.b64encode(public_cert_pem.encode()) + if isinstance(publicKeyData, bytes): + publicKeyData = publicKeyData.decode() + print(' Done generating private key and public certificate.') + return private_pem, publicKeyData + +def _formatOAuth2ServiceData(project_id, client_email, client_id, private_key, private_key_id): + quoted_email = quote(client_email) + key_json = { + 'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs', + 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', + 'client_email': client_email, + 'client_id': client_id, + 'client_x509_cert_url': f'https://www.googleapis.com/robot/v1/metadata/x509/{quoted_email}', + 'private_key': private_key, + 'private_key_id': private_key_id, + 'project_id': project_id, + 'token_uri': 'https://oauth2.googleapis.com/token', + 'type': 'service_account', + } + return json.dumps(key_json, indent=2, sort_keys=True) + +def doShowServiceAccountKeys(): + iam = buildGAPIServiceObject('iam', None) + keyTypes = None + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'all': + keyTypes = None + i += 1 + elif myarg in ['system', 'systemmanaged']: + keyTypes = 'SYSTEM_MANAGED' + i += 1 + elif myarg in ['user', 'usermanaged']: + keyTypes = 'USER_MANAGED' + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam show sakeys") + name = f'projects/-/serviceAccounts/{GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]}' + currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] + keys = gapi.get_items(iam.projects().serviceAccounts().keys(), 'list', 'keys', + name=name, keyTypes=keyTypes) + if not keys: + print('No keys') + return + parts = keys[0]['name'].rsplit('/') + for i in range(0, 4, 2): + print(f'{parts[i][:-1]}: {parts[i+1]}') + for key in keys: + key['name'] = key['name'].rsplit('/', 1)[-1] + if key['name'] == currentPrivateKeyId: + key['usedToAuthenticateThisRequest'] = True + display.print_json(keys) + +def doCreateOrRotateServiceAccountKeys(iam=None, project_id=None, client_email=None, client_id=None): + local_key_size = 2048 + body = {} + if iam: + mode = 'retainexisting' + else: + mode = 'retainnone' + i = 3 + iam = buildGAPIServiceObject('iam', None) + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'algorithm': + body['keyAlgorithm'] = sys.argv[i+1].upper() + allowed_algorithms = gapi.get_enum_values_minus_unspecified(iam._rootDesc['schemas']['CreateServiceAccountKeyRequest']['properties']['keyAlgorithm']['enum']) + if body['keyAlgorithm'] not in allowed_algorithms: + controlflow.expected_argument_exit("algorithm", ", ".join(allowed_algorithms), body['keyAlgorithm']) + local_key_size = 0 + i += 2 + elif myarg == 'localkeysize': + local_key_size = int(sys.argv[i+1]) + if local_key_size not in [1024, 2048, 4096]: + controlflow.system_error_exit(3, 'localkeysize must be 1024, 2048 or 4096. 1024 is weak and dangerous. 2048 is recommended. 4096 is slow.') + i += 2 + elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']: + mode = myarg + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam rotate sakeys") + currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] + project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id'] + client_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email'] + client_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_id'] + clientId = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] + name = f'projects/-/serviceAccounts/{clientId}' + if mode != 'retainexisting': + keys = gapi.get_items(iam.projects().serviceAccounts().keys(), 'list', 'keys', + name=name, keyTypes='USER_MANAGED') + if local_key_size: + private_key, publicKeyData = _generatePrivateKeyAndPublicCert(name, local_key_size) + print(' Uploading new public certificate to Google...') + result = gapi.call(iam.projects().serviceAccounts().keys(), 'upload', + name=name, body={'publicKeyData': publicKeyData}) + private_key_id = result['name'].rsplit('/', 1)[-1] + oauth2service_data = _formatOAuth2ServiceData(project_id, client_email, client_id, private_key, private_key_id) + else: + result = gapi.call(iam.projects().serviceAccounts().keys(), 'create', name=name, body=body) + oauth2service_data = base64.b64decode(result['privateKeyData']).decode(UTF8) + private_key_id = result['name'].rsplit('/', 1)[-1] + fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON], oauth2service_data, continue_on_error=False) + print(f' Wrote new private key {private_key_id} to {GC_Values[GC_OAUTH2SERVICE_JSON]}') + if mode != 'retainexisting': + count = len(keys) if mode == 'retainnone' else 1 + print(f' Revoking {count} existing key(s) for Service Account {clientId}') + for key in keys: + keyName = key['name'].rsplit('/', 1)[-1] + if mode == 'retainnone' or keyName == currentPrivateKeyId: + print(f' Revoking existing key {keyName} for service account') + gapi.call(iam.projects().serviceAccounts().keys(), 'delete', name=key['name']) + if mode != 'retainnone': + break + +def doDeleteServiceAccountKeys(): + iam = buildGAPIServiceObject('iam', None) + doit = False + keyList = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'doit': + doit = True + i += 1 + else: + keyList.extend(sys.argv[i].replace(',', ' ').split()) + i += 1 + clientId = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] + currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['private_key_id'] + name = f'projects/-/serviceAccounts/{clientId}' + keys = gapi.get_items(iam.projects().serviceAccounts().keys(), 'list', 'keys', + name=name, keyTypes='USER_MANAGED') + print(f' Service Account {clientId} has {len(keys)} existing key(s)') + for dkeyName in keyList: + for key in keys: + keyName = key['name'].rsplit('/', 1)[-1] + if dkeyName == keyName: + if keyName == currentPrivateKeyId and not doit: + print(f' Current existing key {keyName} for service account not revoked because doit argument not specified ') + break + print(f' Revoking existing key {keyName} for service account') + gapi.call(iam.projects().serviceAccounts().keys(), 'delete', name=key['name']) + break + else: + print(f' Existing key {dkeyName} for service account not found') + +def doDelProjects(): + crm, _, login_hint, projects, _ = _getLoginHintProjects(False) + count = len(projects) + print(f'User: {login_hint}, Delete {count} Projects') + i = 0 + for project in projects: + i += 1 + projectId = project['projectId'] + try: + gapi.call(crm.projects(), 'delete', throw_reasons=[gapi_errors.ErrorReason.FORBIDDEN], projectId=projectId) + print(f' Project: {projectId} Deleted{currentCount(i, count)}') + except gapi_errors.GapiForbiddenError as e: + print(f' Project: {projectId} Delete Failed: {str(e)}{currentCount(i, count)}') + +def doPrintShowProjects(csvFormat): + _, _, login_hint, projects, i = _getLoginHintProjects(True) + if csvFormat: + csvRows = [] + todrive = False + titles = ['User', 'projectId', 'projectNumber', 'name', 'createTime', 'lifecycleState'] + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} projects") + if not csvFormat: + count = len(projects) + print(f'User: {login_hint}, Show {count} Projects') + i = 0 + for project in projects: + i += 1 + print(f' Project: {project["projectId"]}{currentCount(i, count)}') + print(f' projectNumber: {project["projectNumber"]}') + print(f' name: {project["name"]}') + print(f' createTime: {project["createTime"]}') + print(f' lifecycleState: {project["lifecycleState"]}') + jcount = len(project.get('labels', [])) + if jcount > 0: + print(' labels:') + for k, v in list(project['labels'].items()): + print(f' {k}: {v}') + if 'parent' in project: + print(' parent:') + print(f' type: {project["parent"]["type"]}') + print(f' id: {project["parent"]["id"]}') + else: + for project in projects: + display.add_row_titles_to_csv_file(utils.flatten_json(project, flattened={'User': login_hint}), csvRows, titles) + display.write_csv_file(csvRows, titles, 'Projects', todrive) + +def doGetTeamDriveInfo(users): + teamDriveId = sys.argv[5] + useDomainAdminAccess = False + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam show teamdrive") + for user in users: + drive = buildGAPIServiceObject('drive3', user) + if not drive: + print(f'Failed to access Drive as {user}') + continue + result = gapi.call(drive.drives(), 'get', driveId=teamDriveId, + useDomainAdminAccess=useDomainAdminAccess, fields='*') + display.print_json(result) + +def doCreateTeamDrive(users): + body = {'name': sys.argv[5]} + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'theme': + body['themeId'] = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam create teamdrive") + for user in users: + drive = buildGAPIServiceObject('drive3', user) + if not drive: + print(f'Failed to access Drive as {user}') + continue + requestId = str(uuid.uuid4()) + result = gapi.call(drive.drives(), 'create', requestId=requestId, body=body, fields='id') + print(f'Created Team Drive {body["name"]} with id {result["id"]}') + +TEAMDRIVE_RESTRICTIONS_MAP = { + 'adminmanagedrestrictions': 'adminManagedRestrictions', + 'copyrequireswriterpermission': 'copyRequiresWriterPermission', + 'domainusersonly': 'domainUsersOnly', + 'teammembersonly': 'teamMembersOnly', + } + +def doUpdateTeamDrive(users): + teamDriveId = sys.argv[5] + body = {} + useDomainAdminAccess = False + i = 6 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'name': + body['name'] = sys.argv[i+1] + i += 2 + elif myarg == 'theme': + body['themeId'] = sys.argv[i+1] + i += 2 + elif myarg == 'customtheme': + body['backgroundImageFile'] = { + 'id': sys.argv[i+1], + 'xCoordinate': float(sys.argv[i+2]), + 'yCoordinate': float(sys.argv[i+3]), + 'width': float(sys.argv[i+4]) + } + i += 5 + elif myarg == 'color': + body['colorRgb'] = getColor(sys.argv[i+1]) + i += 2 + elif myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + elif myarg in TEAMDRIVE_RESTRICTIONS_MAP: + body.setdefault('restrictions', {}) + body['restrictions'][TEAMDRIVE_RESTRICTIONS_MAP[myarg]] = getBoolean(sys.argv[i+1], myarg) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam update teamdrive") + if not body: + controlflow.system_error_exit(4, 'nothing to update. Need at least a name argument.') + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + result = gapi.call(drive.drives(), 'update', + useDomainAdminAccess=useDomainAdminAccess, body=body, driveId=teamDriveId, fields='id', soft_errors=True) + if not result: + continue + print(f'Updated Team Drive {teamDriveId}') + +def printShowTeamDrives(users, csvFormat): + todrive = False + useDomainAdminAccess = False + q = None + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'asadmin': + useDomainAdminAccess = True + i += 1 + elif myarg == 'query': + q = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(myarg, "gam print|show teamdrives") + tds = [] + for user in users: + sys.stderr.write(f'Getting Team Drives for {user}\n') + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + results = gapi.get_all_pages(drive.drives(), 'list', 'drives', + useDomainAdminAccess=useDomainAdminAccess, fields='*', + q=q, soft_errors=True) + if not results: + continue + for td in results: + if 'id' not in td: + continue + if 'name' not in td: + td['name'] = '' + this_td = {'id': td['id'], 'name': td['name']} + if this_td in tds: + continue + tds.append({'id': td['id'], 'name': td['name']}) + if csvFormat: + titles = ['name', 'id'] + display.write_csv_file(tds, titles, 'Team Drives', todrive) + else: + for td in tds: + print(f'Name: {td["name"]} ID: {td["id"]}') + +def doDeleteTeamDrive(users): + teamDriveId = sys.argv[5] + for user in users: + user, drive = buildDrive3GAPIObject(user) + if not drive: + continue + print(f'Deleting Team Drive {teamDriveId}') + gapi.call(drive.drives(), 'delete', driveId=teamDriveId, soft_errors=True) + +def extract_nested_zip(zippedFile, toFolder, spacing=' '): + """ Extract a zip file including any nested zip files + Delete the zip file(s) after extraction + """ + print(f'{spacing}extracting {zippedFile}') + with zipfile.ZipFile(zippedFile, 'r') as zfile: + inner_files = zfile.infolist() + for inner_file in inner_files: + print(f'{spacing} {inner_file.filename}') + inner_file_path = zfile.extract(inner_file, toFolder) + if re.search(r'\.zip$', inner_file.filename): + extract_nested_zip(inner_file_path, toFolder, spacing=spacing+' ') + os.remove(zippedFile) + +def doCreateUser(): + cd = buildGAPIObject('directory') + body = getUserAttributes(3, cd, False) + print(f'Creating account for {body["primaryEmail"]}') + gapi.call(cd.users(), 'insert', body=body, fields='primaryEmail') + +def GroupIsAbuseOrPostmaster(emailAddr): + return emailAddr.startswith('abuse@') or emailAddr.startswith('postmaster@') + +GROUP_SETTINGS_LIST_PATTERN = re.compile(r'([A-Z][A-Z_]+[A-Z]?)') + +def getGroupAttrValue(myarg, value, gs_object, gs_body, function): + if myarg == 'collaborative': + myarg = 'enablecollaborativeinbox' + for (attrib, params) in list(gs_object['schemas']['Groups']['properties'].items()): + if attrib in ['kind', 'etag', 'email']: + continue + if myarg == attrib.lower(): + if params['type'] == 'integer': + try: + if value[-1:].upper() == 'M': + value = int(value[:-1]) * 1024 * 1024 + elif value[-1:].upper() == 'K': + value = int(value[:-1]) * 1024 + elif value[-1].upper() == 'B': + value = int(value[:-1]) + else: + value = int(value) + except ValueError: + controlflow.system_error_exit(2, f'{myarg} must be a number ending with M (megabytes), K (kilobytes) or nothing (bytes); got {value}') + elif params['type'] == 'string': + if attrib == 'description': + value = value.replace('\\n', '\n') + elif attrib == 'primaryLanguage': + value = LANGUAGE_CODES_MAP.get(value.lower(), value) + elif attrib in GROUP_SETTINGS_LIST_ATTRIBUTES: + value = value.upper() + possible_values = GROUP_SETTINGS_LIST_PATTERN.findall(params['description']) + if value not in possible_values: + controlflow.expected_argument_exit(f"value for {attrib}", ", ".join(possible_values), value) + elif attrib in GROUP_SETTINGS_BOOLEAN_ATTRIBUTES: + value = value.lower() + if value in true_values: + value = 'true' + elif value in false_values: + value = 'false' + else: + controlflow.expected_argument_exit(f"value for {attrib}", ", ".join(['true', 'false']), value) + gs_body[attrib] = value + return + controlflow.invalid_argument_exit(myarg, f"gam {function} group") + +def doCreateGroup(): + cd = buildGAPIObject('directory') + body = {'email': normalizeEmailAddressOrUID(sys.argv[3], noUid=True)} + gs_get_before_update = got_name = False + i = 4 + gs_body = {} + gs = None + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'name': + body['name'] = sys.argv[i+1] + got_name = True + i += 2 + elif myarg == 'description': + description = sys.argv[i+1].replace('\\n', '\n') +# The Directory API Groups insert method can not handle any of these characters ('\n<>=') in the description field +# If any of these characters are present, use the Group Settings API to set the description + for c in '\n<>=': + if description.find(c) != -1: + gs_body['description'] = description + if not gs: + gs = buildGAPIObject('groupssettings') + gs_object = gs._rootDesc + break + else: + body['description'] = description + i += 2 + elif myarg == 'getbeforeupdate': + gs_get_before_update = True + i += 1 + else: + if not gs: + gs = buildGAPIObject('groupssettings') + gs_object = gs._rootDesc + getGroupAttrValue(myarg, sys.argv[i+1], gs_object, gs_body, 'create') + i += 2 + if not got_name: + body['name'] = body['email'] + print(f'Creating group {body["email"]}') + gapi.call(cd.groups(), 'insert', body=body, fields='email') + if gs and not GroupIsAbuseOrPostmaster(body['email']): + if gs_get_before_update: + current_settings = gapi.call(gs.groups(), 'get', + retry_reasons=[gapi_errors.ErrorReason.SERVICE_LIMIT, gapi_errors.ErrorReason.NOT_FOUND], + groupUniqueId=body['email'], fields='*') + if current_settings is not None: + gs_body = dict(list(current_settings.items()) + list(gs_body.items())) + if gs_body: + gapi.call(gs.groups(), 'update', groupUniqueId=body['email'], + retry_reasons=[gapi_errors.ErrorReason.SERVICE_LIMIT, + gapi_errors.ErrorReason.NOT_FOUND], + body=gs_body) + +def doCreateAlias(): + cd = buildGAPIObject('directory') + body = {'alias': normalizeEmailAddressOrUID(sys.argv[3], noUid=True, noLower=True)} + target_type = sys.argv[4].lower() + if target_type not in ['user', 'group', 'target']: + controlflow.expected_argument_exit("target type", ", ".join(['user', 'group', 'target']), target_type) + targetKey = normalizeEmailAddressOrUID(sys.argv[5]) + print(f'Creating alias {body["alias"]} for {target_type} {targetKey}') + if target_type == 'user': + gapi.call(cd.users().aliases(), 'insert', userKey=targetKey, body=body) + elif target_type == 'group': + gapi.call(cd.groups().aliases(), 'insert', groupKey=targetKey, body=body) + elif target_type == 'target': + try: + gapi.call(cd.users().aliases(), 'insert', throw_reasons=[gapi_errors.ErrorReason.INVALID, gapi_errors.ErrorReason.BAD_REQUEST], userKey=targetKey, body=body) + except (gapi_errors.GapiInvalidError, gapi_errors.GapiBadRequestError): + gapi.call(cd.groups().aliases(), 'insert', groupKey=targetKey, body=body) + +def doCreateOrg(): + cd = buildGAPIObject('directory') + name = getOrgUnitItem(sys.argv[3], pathOnly=True, absolutePath=False) + parent = '' + body = {} + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'description': + body['description'] = sys.argv[i+1].replace('\\n', '\n') + i += 2 + elif myarg == 'parent': + parent = getOrgUnitItem(sys.argv[i+1]) + i += 2 + elif myarg == 'noinherit': + body['blockInheritance'] = True + i += 1 + elif myarg == 'inherit': + body['blockInheritance'] = False + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam create org") + if parent.startswith('id:'): + parent = gapi.call(cd.orgunits(), 'get', + customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=parent, fields='orgUnitPath')['orgUnitPath'] + if parent == '/': + orgUnitPath = parent+name + else: + orgUnitPath = parent+'/'+name + if orgUnitPath.count('/') > 1: + body['parentOrgUnitPath'], body['name'] = orgUnitPath.rsplit('/', 1) + else: + body['parentOrgUnitPath'] = '/' + body['name'] = orgUnitPath[1:] + parent = body['parentOrgUnitPath'] + gapi.call(cd.orgunits(), 'insert', customerId=GC_Values[GC_CUSTOMER_ID], body=body, retry_reasons=[gapi_errors.ErrorReason.DAILY_LIMIT_EXCEEDED]) + print(f'Created OrgUnit {body["name"]}') + +def doUpdateUser(users, i): + cd = buildGAPIObject('directory') + if users is None: + users = [normalizeEmailAddressOrUID(sys.argv[3])] + body = getUserAttributes(i, cd, True) + vfe = 'primaryEmail' in body and body['primaryEmail'][:4].lower() == 'vfe@' + for user in users: + userKey = user + if vfe: + user_primary = gapi.call(cd.users(), 'get', userKey=userKey, fields='primaryEmail,id') + userKey = user_primary['id'] + user_primary = user_primary['primaryEmail'] + user_name, user_domain = splitEmailAddress(user_primary) + body['primaryEmail'] = f'vfe.{user_name}.{random.randint(1, 99999):05d}@{user_domain}' + body['emails'] = [{'type': 'custom', 'customType': 'former_employee', 'primary': False, 'address': user_primary}] + sys.stdout.write(f'updating user {user}...\n') + if body: + gapi.call(cd.users(), 'update', userKey=userKey, body=body) + +def doRemoveUsersAliases(users): + cd = buildGAPIObject('directory') + for user in users: + user_aliases = gapi.call(cd.users(), 'get', userKey=user, fields='aliases,id,primaryEmail') + user_id = user_aliases['id'] + user_primary = user_aliases['primaryEmail'] + if 'aliases' in user_aliases: + print(f'{user_primary} has {len(user_aliases["aliases"])} aliases') + for an_alias in user_aliases['aliases']: + print(f' removing alias {an_alias} for {user_primary}...') + gapi.call(cd.users().aliases(), 'delete', userKey=user_id, alias=an_alias) + else: + print(f'{user_primary} has no aliases') + +def deleteUserFromGroups(users): + cd = buildGAPIObject('directory') + for user in users: + user_groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', userKey=user, fields='groups(id,email)') + jcount = len(user_groups) + print(f'{user} is in {jcount} groups') + j = 0 + for user_group in user_groups: + j += 1 + print(f' removing {user} from {user_group["email"]}{currentCount(j, jcount)}') + gapi.call(cd.members(), 'delete', soft_errors=True, groupKey=user_group['id'], memberKey=user) + print('') + +def checkGroupExists(cd, group, i=0, count=0): + group = normalizeEmailAddressOrUID(group) + try: + return gapi.call(cd.groups(), 'get', + throw_reasons=gapi_errors.GROUP_GET_THROW_REASONS, retry_reasons=gapi_errors.GROUP_GET_RETRY_REASONS, + groupKey=group, fields='email')['email'] + except (gapi_errors.GapiGroupNotFoundError, gapi_errors.GapiDomainNotFoundError, gapi_errors.GapiDomainCannotUseApisError, gapi_errors.GapiForbiddenError, gapi_errors.GapiBadRequestError): + entityUnknownWarning('Group', group, i, count) + return None + +def _checkMemberRoleIsSuspended(member, validRoles, isSuspended): + if validRoles and member.get('role', ROLE_MEMBER) not in validRoles: + return False + if isSuspended is None: + return True + memberStatus = member.get('status', 'UNKNOWN') + if not isSuspended: + return memberStatus != 'SUSPENDED' + return memberStatus == 'SUSPENDED' + +UPDATE_GROUP_SUBCMDS = ['add', 'clear', 'delete', 'remove', 'sync', 'update'] +GROUP_ROLES_MAP = { + 'owner': ROLE_OWNER, 'owners': ROLE_OWNER, + 'manager': ROLE_MANAGER, 'managers': ROLE_MANAGER, + 'member': ROLE_MEMBER, 'members': ROLE_MEMBER, + } +MEMBER_DELIVERY_MAP = { + 'allmail': 'ALL_MAIL', 'digest': 'DIGEST', 'daily': 'DAILY', + 'abridged': 'DAILY', 'nomail': 'NONE', 'none': 'NONE' + } +def doUpdateGroup(): + +# Convert foo@googlemail.com to foo@gmail.com; eliminate periods in name for foo.bar@gmail.com + def _cleanConsumerAddress(emailAddress, mapCleanToOriginal): + atLoc = emailAddress.find('@') + if atLoc > 0: + if emailAddress[atLoc+1:] in ['gmail.com', 'googlemail.com']: + cleanEmailAddress = emailAddress[:atLoc].replace('.', '')+'@gmail.com' + if cleanEmailAddress != emailAddress: + mapCleanToOriginal[cleanEmailAddress] = emailAddress + return cleanEmailAddress + return emailAddress + + def _getRoleAndUsers(): + checkSuspended = None + role = None + delivery = None + i = 5 + if sys.argv[i].lower() in GROUP_ROLES_MAP: + role = GROUP_ROLES_MAP[sys.argv[i].lower()] + i += 1 + if sys.argv[i].lower() in ['suspended', 'notsuspended']: + checkSuspended = sys.argv[i].lower() == 'suspended' + i += 1 + if sys.argv[i].lower().replace('_', '') in MEMBER_DELIVERY_MAP: + delivery = MEMBER_DELIVERY_MAP[sys.argv[i].lower().replace('_', '')] + i += 1 + if sys.argv[i].lower() in usergroup_types: + users_email = getUsersToModify(entity_type=sys.argv[i].lower(), entity=sys.argv[i+1], checkSuspended=checkSuspended, groupUserMembersOnly=False) + else: + users_email = [normalizeEmailAddressOrUID(sys.argv[i], checkForCustomerId=True)] + return (role, users_email, delivery) + + gs_get_before_update = False + cd = buildGAPIObject('directory') + group = sys.argv[3] + myarg = sys.argv[4].lower() + items = [] + if myarg in UPDATE_GROUP_SUBCMDS: + group = normalizeEmailAddressOrUID(group) + if myarg == 'add': + role, users_email, delivery = _getRoleAndUsers() + if not role: + role = ROLE_MEMBER + if not checkGroupExists(cd, group): + return + if len(users_email) > 1: + sys.stderr.write(f'Group: {group}, Will add {len(users_email)} {role}s.\n') + for user_email in users_email: + item = ['gam', 'update', 'group', group, 'add', role] + if delivery: + item.append(delivery) + item.append(user_email) + items.append(item) + else: + body = {'role': role, 'email' if users_email[0].find('@') != -1 else 'id': users_email[0]} + add_text = [f'as {role}'] + if delivery: + body['delivery_settings'] = delivery + add_text.append(f'delivery {delivery}') + for i in range(2): + try: + gapi.call(cd.members(), 'insert', + throw_reasons=[gapi_errors.ErrorReason.DUPLICATE, gapi_errors.ErrorReason.MEMBER_NOT_FOUND, gapi_errors.ErrorReason.RESOURCE_NOT_FOUND, gapi_errors.ErrorReason.INVALID_MEMBER, gapi_errors.ErrorReason.CYCLIC_MEMBERSHIPS_NOT_ALLOWED], + groupKey=group, body=body) + print(f' Group: {group}, {users_email[0]} Added {" ".join(add_text)}') + break + except gapi_errors.GapiDuplicateError as e: + # check if user is a full member, not pending + try: + result = gapi.call(cd.members(), 'get', throw_reasons=[gapi_errors.ErrorReason.MEMBER_NOT_FOUND], memberKey=users_email[0], groupKey=group, fields='role') + print(f' Group: {group}, {users_email[0]} Add {" ".join(add_text)} Failed: Duplicate, already a {result["role"]}') + break # if get succeeds, user is a full member and we throw duplicate error + except gapi_errors.GapiMemberNotFoundError: + # insert fails on duplicate and get fails on not found, user is pending + print(f' Group: {group}, {users_email[0]} member is pending, deleting and re-adding to solve...') + gapi.call(cd.members(), 'delete', memberKey=users_email[0], groupKey=group) + continue # 2nd insert should succeed now that pending is clear + except (gapi_errors.GapiMemberNotFoundError, gapi_errors.GapiResourceNotFoundError, gapi_errors.GapiInvalidMemberError, gapi_errors.GapiCyclicMembershipsNotAllowedError) as e: + print(f' Group: {group}, {users_email[0]} Add {" ".join(add_text)} Failed: {str(e)}') + break + elif myarg == 'sync': + syncMembersSet = set() + syncMembersMap = {} + role, users_email, delivery = _getRoleAndUsers() + for user_email in users_email: + if user_email in ('*', GC_Values[GC_CUSTOMER_ID]): + syncMembersSet.add(GC_Values[GC_CUSTOMER_ID]) + else: + syncMembersSet.add(_cleanConsumerAddress(user_email.lower(), syncMembersMap)) + group = checkGroupExists(cd, group) + if group: + currentMembersSet = set() + currentMembersMap = {} + for current_email in getUsersToModify(entity_type='group', entity=group, member_type=role, groupUserMembersOnly=False): + if current_email == GC_Values[GC_CUSTOMER_ID]: + currentMembersSet.add(current_email) + else: + currentMembersSet.add(_cleanConsumerAddress(current_email.lower(), currentMembersMap)) +# Compare incoming members and current members using the cleaned addresses; we actually add/remove with the original addresses + to_add = [syncMembersMap.get(emailAddress, emailAddress) for emailAddress in syncMembersSet-currentMembersSet] + to_remove = [currentMembersMap.get(emailAddress, emailAddress) for emailAddress in currentMembersSet-syncMembersSet] + sys.stderr.write(f'Group: {group}, Will add {len(to_add)} and remove {len(to_remove)} {role}s.\n') + for user in to_add: + item = ['gam', 'update', 'group', group, 'add'] + if role: + item.append(role) + if delivery: + item.append(delivery) + item.append(user) + items.append(item) + for user in to_remove: + items.append(['gam', 'update', 'group', group, 'remove', user]) + elif myarg in ['delete', 'remove']: + _, users_email, _ = _getRoleAndUsers() + if not checkGroupExists(cd, group): + return + if len(users_email) > 1: + sys.stderr.write(f'Group: {group}, Will remove {len(users_email)} emails.\n') + for user_email in users_email: + items.append(['gam', 'update', 'group', group, 'remove', user_email]) + else: + try: + gapi.call(cd.members(), 'delete', + throw_reasons=[gapi_errors.ErrorReason.MEMBER_NOT_FOUND, gapi_errors.ErrorReason.INVALID_MEMBER], + groupKey=group, memberKey=users_email[0]) + print(f' Group: {group}, {users_email[0]} Removed') + except (gapi_errors.GapiMemberNotFoundError, gapi_errors.GapiInvalidMemberError) as e: + print(f' Group: {group}, {users_email[0]} Remove Failed: {str(e)}') + elif myarg == 'update': + role, users_email, delivery = _getRoleAndUsers() + group = checkGroupExists(cd, group) + if group: + if not role and not delivery: + role = ROLE_MEMBER + if len(users_email) > 1: + sys.stderr.write(f'Group: {group}, Will update {len(users_email)} {role}s.\n') + for user_email in users_email: + item = ['gam', 'update', 'group', group, 'update'] + if role: + item.append(role) + if delivery: + item.append(delivery) + item.append(user_email) + items.append(item) + else: + body = {} + update_text = [] + if role: + body['role'] = role + update_text.append(f'to {role}') + if delivery: + body['delivery_settings'] = delivery + update_text.append(f'delivery {delivery}') + try: + gapi.call(cd.members(), 'update', + throw_reasons=[gapi_errors.ErrorReason.MEMBER_NOT_FOUND, gapi_errors.ErrorReason.INVALID_MEMBER], + groupKey=group, memberKey=users_email[0], body=body) + print(f' Group: {group}, {users_email[0]} Updated {" ".join(update_text)}') + except (gapi_errors.GapiMemberNotFoundError, gapi_errors.GapiInvalidMemberError) as e: + print(f' Group: {group}, {users_email[0]} Update to {role} Failed: {str(e)}') + else: # clear + checkSuspended = None + fields = ['email', 'id'] + roles = [] + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg.upper() in [ROLE_OWNER, ROLE_MANAGER, ROLE_MEMBER]: + roles.append(myarg.upper()) + i += 1 + elif myarg in ['suspended', 'notsuspended']: + checkSuspended = myarg == 'suspended' + fields.append('status') + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam update group clear") + if roles: + roles = ','.join(sorted(set(roles))) + else: + roles = ROLE_MEMBER + group = normalizeEmailAddressOrUID(group) + member_type_message = f'{roles.lower()}s' + sys.stderr.write(f'Getting {member_type_message} of {group} (may take some time for large groups)...\n') + page_message = gapi.got_total_items_msg(f'{member_type_message}', '...') + validRoles, listRoles, listFields = _getRoleVerification(roles, f'nextPageToken,members({",".join(fields)})') + try: + result = gapi.get_all_pages(cd.members(), 'list', 'members', + page_message=page_message, + throw_reasons=gapi_errors.MEMBERS_THROW_REASONS, + groupKey=group, roles=listRoles, fields=listFields) + if not result: + print('Group already has 0 members') + return + users_email = [member.get('email', member['id']) for member in result if _checkMemberRoleIsSuspended(member, validRoles, checkSuspended)] + if len(users_email) > 1: + sys.stderr.write(f'Group: {group}, Will remove {len(users_email)} {"" if checkSuspended is None else ["Non-suspended ", "Suspended "][checkSuspended]}{roles}s.\n') + for user_email in users_email: + items.append(['gam', 'update', 'group', group, 'remove', user_email]) + else: + try: + gapi.call(cd.members(), 'delete', + throw_reasons=[gapi_errors.ErrorReason.MEMBER_NOT_FOUND, gapi_errors.ErrorReason.INVALID_MEMBER], + groupKey=group, memberKey=users_email[0]) + print(f' Group: {group}, {users_email[0]} Removed') + except (gapi_errors.GapiMemberNotFoundError, gapi_errors.GapiInvalidMemberError) as e: + print(f' Group: {group}, {users_email[0]} Remove Failed: {str(e)}') + except (gapi_errors.GapiGroupNotFoundError, gapi_errors.GapiDomainNotFoundError, gapi_errors.GapiInvalidError, gapi_errors.GapiForbiddenError): + entityUnknownWarning('Group', group, 0, 0) + if items: + run_batch(items) + else: + i = 4 + use_cd_api = False + gs = None + gs_body = {} + cd_body = {} + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'email': + use_cd_api = True + cd_body['email'] = normalizeEmailAddressOrUID(sys.argv[i+1]) + i += 2 + elif myarg == 'admincreated': + use_cd_api = True + cd_body['adminCreated'] = getBoolean(sys.argv[i+1], myarg) + i += 2 + elif myarg == 'getbeforeupdate': + gs_get_before_update = True + i += 1 + else: + if not gs: + gs = buildGAPIObject('groupssettings') + gs_object = gs._rootDesc + getGroupAttrValue(myarg, sys.argv[i+1], gs_object, gs_body, 'update') + i += 2 + group = normalizeEmailAddressOrUID(group) + if use_cd_api or (group.find('@') == -1): # group settings API won't take uid so we make sure cd API is used so that we can grab real email. + group = gapi.call(cd.groups(), 'update', groupKey=group, body=cd_body, fields='email')['email'] + if gs: + if not GroupIsAbuseOrPostmaster(group): + if gs_get_before_update: + current_settings = gapi.call(gs.groups(), 'get', + retry_reasons=[gapi_errors.ErrorReason.SERVICE_LIMIT], + groupUniqueId=group, fields='*') + if current_settings is not None: + gs_body = dict(list(current_settings.items()) + list(gs_body.items())) + if gs_body: + gapi.call(gs.groups(), 'update', retry_reasons=[gapi_errors.ErrorReason.SERVICE_LIMIT], groupUniqueId=group, body=gs_body) + print(f'updated group {group}') + +def doUpdateAlias(): + cd = buildGAPIObject('directory') + alias = normalizeEmailAddressOrUID(sys.argv[3], noUid=True, noLower=True) + target_type = sys.argv[4].lower() + if target_type not in ['user', 'group', 'target']: + controlflow.expected_argument_exit("target type", ", ".join(['user', 'group', 'target']), target_type) + target_email = normalizeEmailAddressOrUID(sys.argv[5]) + try: + gapi.call(cd.users().aliases(), 'delete', throw_reasons=[gapi_errors.ErrorReason.INVALID], userKey=alias, alias=alias) + except gapi_errors.GapiInvalidError: + gapi.call(cd.groups().aliases(), 'delete', groupKey=alias, alias=alias) + if target_type == 'user': + gapi.call(cd.users().aliases(), 'insert', userKey=target_email, body={'alias': alias}) + elif target_type == 'group': + gapi.call(cd.groups().aliases(), 'insert', groupKey=target_email, body={'alias': alias}) + elif target_type == 'target': + try: + gapi.call(cd.users().aliases(), 'insert', throw_reasons=[gapi_errors.ErrorReason.INVALID], userKey=target_email, body={'alias': alias}) + except gapi_errors.GapiInvalidError: + gapi.call(cd.groups().aliases(), 'insert', groupKey=target_email, body={'alias': alias}) + print(f'updated alias {alias}') + +def doUpdateMobile(): + cd = buildGAPIObject('directory') + resourceIds = sys.argv[3] + match_users = None + doit = False + if resourceIds[:6] == 'query:': + query = resourceIds[6:] + fields = 'nextPageToken,mobiledevices(resourceId,email)' + page_message = gapi.got_total_items_msg('Mobile Devices', '...\n') + devices = gapi.get_all_pages(cd.mobiledevices(), 'list', + page_message=page_message, + customerId=GC_Values[GC_CUSTOMER_ID], + items='mobiledevices', query=query, fields=fields) + else: + devices = [{'resourceId': resourceIds, 'email': ['not set']}] + doit = True + i = 4 + body = {} + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'action': + body['action'] = sys.argv[i+1].lower() + validActions = ['wipe', 'wipeaccount', 'accountwipe', 'wipe_account', 'account_wipe', 'approve', 'block', 'cancel_remote_wipe_then_activate', 'cancel_remote_wipe_then_block'] + if body['action'] not in validActions: + controlflow.expected_argument_exit("action", ", ".join(validActions), body['action']) + if body['action'] == 'wipe': + body['action'] = 'admin_remote_wipe' + elif body['action'].replace('_', '') in ['accountwipe', 'wipeaccount']: + body['action'] = 'admin_account_wipe' + i += 2 + elif myarg in ['ifusers', 'matchusers']: + match_users = getUsersToModify(entity_type=sys.argv[i+1].lower(), entity=sys.argv[i+2]) + i += 3 + elif myarg == 'doit': + doit = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam update mobile") + if body: + if doit: + print(f'Updating {len(devices)} devices') + describe_as = 'Performing' + else: + print(f'Showing {len(devices)} changes that would be made, not actually making changes because doit argument not specified') + describe_as = 'Would perform' + for device in devices: + device_user = device.get('email', [''])[0] + if match_users and device_user not in match_users: + print(f'Skipping device for user {device_user} that did not match match_users argument') + else: + print(f'{describe_as} {body["action"]} on user {device_user} device {device["resourceId"]}') + if doit: + gapi.call(cd.mobiledevices(), 'action', resourceId=device['resourceId'], body=body, customerId=GC_Values[GC_CUSTOMER_ID]) + +def doDeleteMobile(): + cd = buildGAPIObject('directory') + resourceId = sys.argv[3] + gapi.call(cd.mobiledevices(), 'delete', resourceId=resourceId, customerId=GC_Values[GC_CUSTOMER_ID]) + +def doUpdateOrg(): + cd = buildGAPIObject('directory') + orgUnitPath = getOrgUnitItem(sys.argv[3]) + if sys.argv[4].lower() in ['move', 'add']: + entity_type = sys.argv[5].lower() + if entity_type in usergroup_types: + users = getUsersToModify(entity_type=entity_type, entity=sys.argv[6]) + else: + entity_type = 'users' + users = getUsersToModify(entity_type=entity_type, entity=sys.argv[5]) + if (entity_type.startswith('cros')) or ((entity_type == 'all') and (sys.argv[6].lower() == 'cros')): + for l in range(0, len(users), 50): + move_body = {'deviceIds': users[l:l+50]} + print(f' moving {len(move_body["deviceIds"])} devices to {orgUnitPath}') + gapi.call(cd.chromeosdevices(), 'moveDevicesToOu', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=orgUnitPath, body=move_body) + else: + i = 0 + count = len(users) + for user in users: + i += 1 + sys.stderr.write(f' moving {user} to {orgUnitPath}{currentCountNL(i, count)}') + try: + gapi.call(cd.users(), 'update', throw_reasons=[gapi_errors.ErrorReason.CONDITION_NOT_MET], userKey=user, body={'orgUnitPath': orgUnitPath}) + except gapi_errors.GapiConditionNotMetError: + pass + else: + body = {} + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'name': + body['name'] = sys.argv[i+1] + i += 2 + elif myarg == 'description': + body['description'] = sys.argv[i+1].replace('\\n', '\n') + i += 2 + elif myarg == 'parent': + parent = getOrgUnitItem(sys.argv[i+1]) + if parent.startswith('id:'): + body['parentOrgUnitId'] = parent + else: + body['parentOrgUnitPath'] = parent + i += 2 + elif myarg == 'noinherit': + body['blockInheritance'] = True + i += 1 + elif myarg == 'inherit': + body['blockInheritance'] = False + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam update org") + gapi.call(cd.orgunits(), 'update', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)), body=body) + +def doWhatIs(): + cd = buildGAPIObject('directory') + email = normalizeEmailAddressOrUID(sys.argv[2]) + try: + user_or_alias = gapi.call(cd.users(), 'get', throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND, gapi_errors.ErrorReason.BAD_REQUEST, gapi_errors.ErrorReason.INVALID], userKey=email, fields='id,primaryEmail') + if (user_or_alias['primaryEmail'].lower() == email) or (user_or_alias['id'] == email): + sys.stderr.write(f'{email} is a user\n\n') + doGetUserInfo(user_email=email) + return + sys.stderr.write(f'{email} is a user alias\n\n') + doGetAliasInfo(alias_email=email) + return + except (gapi_errors.GapiNotFoundError, gapi_errors.GapiBadRequestError, gapi_errors.GapiInvalidError): + sys.stderr.write(f'{email} is not a user...\n') + sys.stderr.write(f'{email} is is not a user alias...\n') + try: + group = gapi.call(cd.groups(), 'get', throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND, gapi_errors.ErrorReason.BAD_REQUEST], groupKey=email, fields='id,email') + except (gapi_errors.GapiNotFoundError, gapi_errors.GapiBadRequestError): + controlflow.system_error_exit(1, f'{email} is not a group either!\n\nDoesn\'t seem to exist!\n\n') + if (group['email'].lower() == email) or (group['id'] == email): + sys.stderr.write(f'{email} is a group\n\n') + doGetGroupInfo(group_name=email) + else: + sys.stderr.write(f'{email} is a group alias\n\n') + doGetAliasInfo(alias_email=email) + +def convertSKU2ProductId(res, sku, customerId): + results = gapi.call(res.subscriptions(), 'list', customerId=customerId) + for subscription in results['subscriptions']: + if sku == subscription['skuId']: + return subscription['subscriptionId'] + controlflow.system_error_exit(3, f'could not find subscription for customer {customerId} and SKU {sku}') + +def doDeleteResoldSubscription(): + res = buildGAPIObject('reseller') + customerId = sys.argv[3] + sku = sys.argv[4] + deletionType = sys.argv[5] + subscriptionId = convertSKU2ProductId(res, sku, customerId) + gapi.call(res.subscriptions(), 'delete', customerId=customerId, subscriptionId=subscriptionId, deletionType=deletionType) + print(f'Cancelled {sku} for {customerId}') + +def doCreateResoldSubscription(): + res = buildGAPIObject('reseller') + customerId = sys.argv[3] + customerAuthToken, body = _getResoldSubscriptionAttr(sys.argv[4:], customerId) + result = gapi.call(res.subscriptions(), 'insert', customerId=customerId, customerAuthToken=customerAuthToken, body=body, fields='customerId') + print('Created subscription:') + display.print_json(result) + +def doUpdateResoldSubscription(): + res = buildGAPIObject('reseller') + function = None + customerId = sys.argv[3] + sku = sys.argv[4] + subscriptionId = convertSKU2ProductId(res, sku, customerId) + kwargs = {} + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'activate': + function = 'activate' + i += 1 + elif myarg == 'suspend': + function = 'suspend' + i += 1 + elif myarg == 'startpaidservice': + function = 'startPaidService' + i += 1 + elif myarg in ['renewal', 'renewaltype']: + function = 'changeRenewalSettings' + kwargs['body'] = {'renewalType': sys.argv[i+1].upper()} + i += 2 + elif myarg in ['seats']: + function = 'changeSeats' + kwargs['body'] = {'numberOfSeats': getInteger(sys.argv[i+1], 'numberOfSeats', minVal=0)} + if len(sys.argv) > i + 2 and sys.argv[i+2].isdigit(): + kwargs['body']['maximumNumberOfSeats'] = getInteger(sys.argv[i+2], 'maximumNumberOfSeats', minVal=0) + i += 3 + else: + i += 2 + elif myarg in ['plan']: + function = 'changePlan' + kwargs['body'] = {'planName': sys.argv[i+1].upper()} + i += 2 + while i < len(sys.argv): + planarg = sys.argv[i].lower() + if planarg == 'seats': + kwargs['body']['seats'] = {'numberOfSeats': getInteger(sys.argv[i+1], 'numberOfSeats', minVal=0)} + if len(sys.argv) > i + 2 and sys.argv[i+2].isdigit(): + kwargs['body']['seats']['maximumNumberOfSeats'] = getInteger(sys.argv[i+2], 'maximumNumberOfSeats', minVal=0) + i += 3 + else: + i += 2 + elif planarg in ['purchaseorderid', 'po']: + kwargs['body']['purchaseOrderId'] = sys.argv[i+1] + i += 2 + elif planarg in ['dealcode', 'deal']: + kwargs['body']['dealCode'] = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(planarg, "gam update resoldsubscription plan") + else: + controlflow.invalid_argument_exit(myarg, "gam update resoldsubscription") + result = gapi.call(res.subscriptions(), function, customerId=customerId, subscriptionId=subscriptionId, **kwargs) + print(f'Updated {customerId} SKU {sku} subscription:') + if result: + display.print_json(result) + +def doGetResoldSubscriptions(): + res = buildGAPIObject('reseller') + customerId = sys.argv[3] + customerAuthToken = None + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg in ['customerauthtoken', 'transfertoken']: + customerAuthToken = sys.argv[i+1] + i += 2 + else: + controlflow.invalid_argument_exit(myarg, "gam info resoldsubscriptions") + result = gapi.call(res.subscriptions(), 'list', customerId=customerId, customerAuthToken=customerAuthToken) + display.print_json(result) + +def _getResoldSubscriptionAttr(arg, customerId): + body = {'plan': {}, + 'seats': {}, + 'customerId': customerId} + customerAuthToken = None + i = 0 + while i < len(arg): + myarg = arg[i].lower().replace('_', '') + if myarg in ['deal', 'dealcode']: + body['dealCode'] = arg[i+1] + elif myarg in ['plan', 'planname']: + body['plan']['planName'] = arg[i+1].upper() + elif myarg in ['purchaseorderid', 'po']: + body['purchaseOrderId'] = arg[i+1] + elif myarg in ['seats']: + body['seats']['numberOfSeats'] = getInteger(sys.argv[i+1], 'numberOfSeats', minVal=0) + if len(arg) > i + 2 and arg[i+2].isdigit(): + body['seats']['maximumNumberOfSeats'] = getInteger(sys.argv[i+2], 'maximumNumberOfSeats', minVal=0) + i += 1 + elif myarg in ['sku', 'skuid']: + _, body['skuId'] = getProductAndSKU(arg[i+1]) + elif myarg in ['customerauthtoken', 'transfertoken']: + customerAuthToken = arg[i+1] + else: + controlflow.invalid_argument_exit(myarg, "gam create resoldsubscription") + i += 2 + return customerAuthToken, body + +def doGetResoldCustomer(): + res = buildGAPIObject('reseller') + customerId = sys.argv[3] + result = gapi.call(res.customers(), 'get', customerId=customerId) + display.print_json(result) + +def _getResoldCustomerAttr(arg): + body = {} + customerAuthToken = None + i = 0 + while i < len(arg): + myarg = arg[i].lower().replace('_', '') + if myarg in ADDRESS_FIELDS_ARGUMENT_MAP: + body.setdefault('postalAddress', {}) + body['postalAddress'][ADDRESS_FIELDS_ARGUMENT_MAP[myarg]] = arg[i+1] + elif myarg in ['email', 'alternateemail']: + body['alternateEmail'] = arg[i+1] + elif myarg in ['phone', 'phonenumber']: + body['phoneNumber'] = arg[i+1] + elif myarg in ['customerauthtoken', 'transfertoken']: + customerAuthToken = arg[i+1] + else: + controlflow.invalid_argument_exit(myarg, f"gam {sys.argv[1]} resoldcustomer") + i += 2 + return customerAuthToken, body + +def doUpdateResoldCustomer(): + res = buildGAPIObject('reseller') + customerId = sys.argv[3] + customerAuthToken, body = _getResoldCustomerAttr(sys.argv[4:]) + gapi.call(res.customers(), 'patch', customerId=customerId, body=body, customerAuthToken=customerAuthToken, fields='customerId') + print(f'updated customer {customerId}') + +def doCreateResoldCustomer(): + res = buildGAPIObject('reseller') + customerAuthToken, body = _getResoldCustomerAttr(sys.argv[4:]) + body['customerDomain'] = sys.argv[3] + result = gapi.call(res.customers(), 'insert', body=body, customerAuthToken=customerAuthToken, fields='customerId,customerDomain') + print(f'Created customer {result["customerDomain"]} with id {result["customerId"]}') + +def _getValueFromOAuth(field, credentials=None): + if not credentials: + credentials = auth.get_admin_credentials() + return credentials.get_token_value(field) + +def doGetMemberInfo(): + cd = buildGAPIObject('directory') + memberKey = normalizeEmailAddressOrUID(sys.argv[3]) + groupKey = normalizeEmailAddressOrUID(sys.argv[4]) + info = gapi.call(cd.members(), 'get', memberKey=memberKey, groupKey=groupKey) + display.print_json(info) + +def doGetUserInfo(user_email=None): + + def user_lic_result(request_id, response, exception): + if response and 'skuId' in response: + user_licenses.append(response['skuId']) + + cd = buildGAPIObject('directory') + i = 3 + if user_email is None: + if len(sys.argv) > 3: + user_email = normalizeEmailAddressOrUID(sys.argv[3]) + i = 4 + else: + user_email = _getValueFromOAuth('email') + getSchemas = getAliases = getGroups = getLicenses = True + projection = 'full' + customFieldMask = viewType = None + skus = sorted(SKUS) + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'noaliases': + getAliases = False + i += 1 + elif myarg == 'nogroups': + getGroups = False + i += 1 + elif myarg in ['nolicenses', 'nolicences']: + getLicenses = False + i += 1 + elif myarg in ['sku', 'skus']: + skus = sys.argv[i+1].split(',') + i += 2 + elif myarg == 'noschemas': + getSchemas = False + projection = 'basic' + i += 1 + elif myarg in ['custom', 'schemas']: + getSchemas = True + projection = 'custom' + customFieldMask = sys.argv[i+1] + i += 2 + elif myarg == 'userview': + viewType = 'domain_public' + getGroups = getLicenses = False + i += 1 + elif myarg in ['nousers', 'groups']: + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam info user") + user = gapi.call(cd.users(), 'get', userKey=user_email, projection=projection, customFieldMask=customFieldMask, viewType=viewType) + print(f'User: {user["primaryEmail"]}') + if 'name' in user and 'givenName' in user['name']: + print(f'First Name: {user["name"]["givenName"]}') + if 'name' in user and 'familyName' in user['name']: + print(f'Last Name: {user["name"]["familyName"]}') + if 'languages' in user: + up = 'languageCode' + languages = [row[up] for row in user['languages'] if up in row] + if languages: + print(f'Languages: {",".join(languages)}') + up = 'customLanguage' + languages = [row[up] for row in user['languages'] if up in row] + if languages: + print(f'Custom Languages: {",".join(languages)}') + if 'isAdmin' in user: + print(f'Is a Super Admin: {user["isAdmin"]}') + if 'isDelegatedAdmin' in user: + print(f'Is Delegated Admin: {user["isDelegatedAdmin"]}') + if 'isEnrolledIn2Sv' in user: + print(f'2-step enrolled: {user["isEnrolledIn2Sv"]}') + if 'isEnforcedIn2Sv' in user: + print(f'2-step enforced: {user["isEnforcedIn2Sv"]}') + if 'agreedToTerms' in user: + print(f'Has Agreed to Terms: {user["agreedToTerms"]}') + if 'ipWhitelisted' in user: + print(f'IP Whitelisted: {user["ipWhitelisted"]}') + if 'suspended' in user: + print(f'Account Suspended: {user["suspended"]}') + if 'suspensionReason' in user: + print(f'Suspension Reason: {user["suspensionReason"]}') + if 'archived' in user: + print(f'Is Archived: {user["archived"]}') + if 'changePasswordAtNextLogin' in user: + print(f'Must Change Password: {user["changePasswordAtNextLogin"]}') + if 'id' in user: + print(f'Google Unique ID: {user["id"]}') + if 'customerId' in user: + print(f'Customer ID: {user["customerId"]}') + if 'isMailboxSetup' in user: + print(f'Mailbox is setup: {user["isMailboxSetup"]}') + if 'includeInGlobalAddressList' in user: + print(f'Included in GAL: {user["includeInGlobalAddressList"]}') + if 'creationTime' in user: + print(f'Creation Time: {user["creationTime"]}') + if 'lastLoginTime' in user: + if user['lastLoginTime'] == NEVER_TIME: + print('Last login time: Never') + else: + print(f'Last login time: {user["lastLoginTime"]}') + if 'orgUnitPath' in user: + print(f'Google Org Unit Path: {user["orgUnitPath"]}') + if 'thumbnailPhotoUrl' in user: + print(f'Photo URL: {user["thumbnailPhotoUrl"]}') + if 'recoveryPhone' in user: + print(f'Recovery Phone: {user["recoveryPhone"]}') + if 'recoveryEmail' in user: + print(f'Recovery Email: {user["recoveryEmail"]}') + if 'notes' in user: + print('Notes:') + notes = user['notes'] + if isinstance(notes, dict): + contentType = notes.get('contentType', 'text_plain') + print(f' contentType: {contentType}') + if contentType == 'text_html': + print(utils.indentMultiLineText(f' value: {utils.dehtml(notes["value"])}', n=2)) + else: + print(utils.indentMultiLineText(f' value: {notes["value"]}', n=2)) + else: + print(utils.indentMultiLineText(f' value: {notes}', n=2)) + print('') + if 'gender' in user: + print('Gender') + gender = user['gender'] + for key in gender: + if key == 'customGender' and not gender[key]: + continue + print(f' {key}: {gender[key]}') + print('') + if 'keywords' in user: + print('Keywords:') + for keyword in user['keywords']: + for key in keyword: + if key == 'customType' and not keyword[key]: + continue + print(f' {key}: {keyword[key]}') + print('') + if 'ims' in user: + print('IMs:') + for im in user['ims']: + for key in im: + print(f' {key}: {im[key]}') + print('') + if 'addresses' in user: + print('Addresses:') + for address in user['addresses']: + for key in address: + if key != 'formatted': + print(f' {key}: {address[key]}') + else: + addr = address[key].replace("\n", "\\n") + print(f' {key}: {addr}') + print('') + if 'organizations' in user: + print('Organizations:') + for org in user['organizations']: + for key in org: + if key == 'customType' and not org[key]: + continue + print(f' {key}: {org[key]}') + print('') + if 'locations' in user: + print('Locations:') + for location in user['locations']: + for key in location: + if key == 'customType' and not location[key]: + continue + print(f' {key}: {location[key]}') + print('') + if 'sshPublicKeys' in user: + print('SSH Public Keys:') + for sshkey in user['sshPublicKeys']: + for key in sshkey: + print(f' {key}: {sshkey[key]}') + print('') + if 'posixAccounts' in user: + print('Posix Accounts:') + for posix in user['posixAccounts']: + for key in posix: + print(f' {key}: {posix[key]}') + print('') + if 'phones' in user: + print('Phones:') + for phone in user['phones']: + for key in phone: + print(f' {key}: {phone[key]}') + print('') + if 'emails' in user: + if len(user['emails']) > 1: + print('Other Emails:') + for an_email in user['emails']: + if an_email['address'].lower() == user['primaryEmail'].lower(): + continue + for key in an_email: + if key == 'type' and an_email[key] == 'custom': + continue + if key == 'customType': + print(f' type: {an_email[key]}') + else: + print(f' {key}: {an_email[key]}') + print('') + if 'relations' in user: + print('Relations:') + for relation in user['relations']: + for key in relation: + if key == 'type' and relation[key] == 'custom': + continue + if key == 'customType': + print(f' type: {relation[key]}') + else: + print(f' {key}: {relation[key]}') + print('') + if 'externalIds' in user: + print('External IDs:') + for externalId in user['externalIds']: + for key in externalId: + if key == 'type' and externalId[key] == 'custom': + continue + if key == 'customType': + print(f' typw: {externalId[key]}') + else: + print(f' {key}: {externalId[key]}') + print('') + if 'websites' in user: + print('Websites:') + for website in user['websites']: + for key in website: + if key == 'type' and website[key] == 'custom': + continue + if key == 'customType': + print(f' type: {website[key]}') + else: + print(f' {key}: {website[key]}') + print('') + if getSchemas: + if 'customSchemas' in user: + print('Custom Schemas:') + for schema in user['customSchemas']: + print(f' Schema: {schema}') + for field in user['customSchemas'][schema]: + if isinstance(user['customSchemas'][schema][field], list): + print(f' {field}:') + for an_item in user['customSchemas'][schema][field]: + print(f' type: {an_item["type"]}') + if an_item['type'] == 'custom': + print(f' customType: {an_item["customType"]}') + print(f' value: {an_item["value"]}') + else: + print(f' {field}: {user["customSchemas"][schema][field]}') + print() + if getAliases: + if 'aliases' in user: + print('Email Aliases:') + for alias in user['aliases']: + print(f' {alias}') + if 'nonEditableAliases' in user: + print('Non-Editable Aliases:') + for alias in user['nonEditableAliases']: + print(f' {alias}') + if getGroups: + groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', userKey=user_email, fields='groups(name,email),nextPageToken') + if groups: + print(f'Groups: ({len(groups)})') + for group in groups: + print(f' {group["name"]} <{group["email"]}>') + if getLicenses: + print('Licenses:') + lic = buildGAPIObject('licensing') + lbatch = lic.new_batch_http_request(callback=user_lic_result) + user_licenses = [] + for sku in skus: + productId, skuId = getProductAndSKU(sku) + lbatch.add(lic.licenseAssignments().get(userId=user_email, productId=productId, skuId=skuId, fields='skuId')) + lbatch.execute() + for user_license in user_licenses: + print(f' {_formatSKUIdDisplayName(user_license)}') + +def _skuIdToDisplayName(skuId): + return SKUS[skuId]['displayName'] if skuId in SKUS else skuId + +def _formatSKUIdDisplayName(skuId): + skuIdDisplay = _skuIdToDisplayName(skuId) + if skuId == skuIdDisplay: + return skuId + return f'{skuId} ({skuIdDisplay})' + +def doGetGroupInfo(group_name=None): + cd = buildGAPIObject('directory') + gs = buildGAPIObject('groupssettings') + getAliases = getUsers = True + getGroups = False + if group_name is None: + group_name = normalizeEmailAddressOrUID(sys.argv[3]) + i = 4 + else: + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'nousers': + getUsers = False + i += 1 + elif myarg == 'noaliases': + getAliases = False + i += 1 + elif myarg == 'groups': + getGroups = True + i += 1 + elif myarg in ['nogroups', 'nolicenses', 'nolicences', 'noschemas', 'schemas', 'userview']: + i += 1 + if myarg == 'schemas': + i += 1 + else: + controlflow.invalid_argument_exit(myarg, "gam info group") + basic_info = gapi.call(cd.groups(), 'get', groupKey=group_name) + settings = {} + if not GroupIsAbuseOrPostmaster(basic_info['email']): + try: + settings = gapi.call(gs.groups(), 'get', throw_reasons=[gapi_errors.ErrorReason.AUTH_ERROR], retry_reasons=[gapi_errors.ErrorReason.SERVICE_LIMIT], + groupUniqueId=basic_info['email']) # Use email address retrieved from cd since GS API doesn't support uid + if settings is None: + settings = {} + except gapi_errors.GapiAuthErrorError: + pass + print('') + print('Group Settings:') + for key, value in list(basic_info.items()): + if (key in ['kind', 'etag']) or ((key == 'aliases') and (not getAliases)): + continue + if isinstance(value, list): + print(f' {key}:') + for val in value: + print(f' {val}') + else: + print(f' {key}: {value}') + for key, value in list(settings.items()): + if key in ['kind', 'etag', 'description', 'email', 'name']: + continue + print(f' {key}: {value}') + if getGroups: + groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', + userKey=basic_info['email'], fields='nextPageToken,groups(name,email)') + if groups: + print(f'Groups: ({len(groups)})') + for groupm in groups: + print(f' {groupm["name"]}: {groupm["email"]}') + if getUsers: + members = gapi.get_all_pages(cd.members(), 'list', 'members', groupKey=group_name, fields='nextPageToken,members(email,id,role,type)') + print('Members:') + for member in members: + print(f' {member.get("role", ROLE_MEMBER).lower()}: {member.get("email", member["id"])} ({member["type"].lower()})') + print(f'Total {len(members)} users in group') + +def doGetAliasInfo(alias_email=None): + cd = buildGAPIObject('directory') + if alias_email is None: + alias_email = normalizeEmailAddressOrUID(sys.argv[3]) + try: + result = gapi.call(cd.users(), 'get', throw_reasons=[gapi_errors.ErrorReason.INVALID, gapi_errors.ErrorReason.BAD_REQUEST], userKey=alias_email) + except (gapi_errors.GapiInvalidError, gapi_errors.GapiBadRequestError): + result = gapi.call(cd.groups(), 'get', groupKey=alias_email) + print(f' Alias Email: {alias_email}') + try: + if result['primaryEmail'].lower() == alias_email.lower(): + controlflow.system_error_exit(3, f'{alias_email} is a primary user email address, not an alias.') + print(f' User Email: {result["primaryEmail"]}') + except KeyError: + print(f' Group Email: {result["email"]}') + print(f' Unique ID: {result["id"]}') + +def doGetMobileInfo(): + cd = buildGAPIObject('directory') + resourceId = sys.argv[3] + info = gapi.call(cd.mobiledevices(), 'get', customerId=GC_Values[GC_CUSTOMER_ID], resourceId=resourceId) + if 'deviceId' in info: + info['deviceId'] = info['deviceId'].encode('unicode-escape').decode(UTF8) + attrib = 'securityPatchLevel' + if attrib in info and int(info[attrib]): + info[attrib] = utils.formatTimestampYMDHMS(info[attrib]) + display.print_json(info) + +def doSiteVerifyShow(): + verif = buildGAPIObject('siteVerification') + a_domain = sys.argv[3] + txt_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'INET_DOMAIN', 'identifier':a_domain}, 'verificationMethod':'DNS_TXT'}) + print(f'TXT Record Name: {a_domain}') + print(f'TXT Record Value: {txt_record["token"]}') + print() + cname_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'INET_DOMAIN', 'identifier':a_domain}, 'verificationMethod':'DNS_CNAME'}) + cname_token = cname_record['token'] + cname_list = cname_token.split(' ') + cname_subdomain = cname_list[0] + cname_value = cname_list[1] + print(f'CNAME Record Name: {cname_subdomain}.{a_domain}') + print(f'CNAME Record Value: {cname_value}') + print('') + webserver_file_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'SITE', 'identifier':f'http://{a_domain}/'}, 'verificationMethod':'FILE'}) + webserver_file_token = webserver_file_record['token'] + print(f'Saving web server verification file to: {webserver_file_token}') + fileutils.write_file(webserver_file_token, f'google-site-verification: {webserver_file_token}', continue_on_error=True) + print(f'Verification File URL: http://{a_domain}/{webserver_file_token}') + print() + webserver_meta_record = gapi.call(verif.webResource(), 'getToken', body={'site':{'type':'SITE', 'identifier':f'http://{a_domain}/'}, 'verificationMethod':'META'}) + print(f'Meta URL: http://{a_domain}/') + print(f'Meta HTML Header Data: {webserver_meta_record["token"]}') + print() + +def doGetSiteVerifications(): + verif = buildGAPIObject('siteVerification') + sites = gapi.get_items(verif.webResource(), 'list', 'items') + if sites: + for site in sites: + print(f'Site: {site["site"]["identifier"]}') + print(f'Type: {site["site"]["type"]}') + print('Owners:') + for owner in site['owners']: + print(f' {owner}') + print() + else: + print('No Sites Verified.') + +def doSiteVerifyAttempt(): + verif = buildGAPIObject('siteVerification') + a_domain = sys.argv[3] + verificationMethod = sys.argv[4].upper() + if verificationMethod == 'CNAME': + verificationMethod = 'DNS_CNAME' + elif verificationMethod in ['TXT', 'TEXT']: + verificationMethod = 'DNS_TXT' + if verificationMethod in ['DNS_TXT', 'DNS_CNAME']: + verify_type = 'INET_DOMAIN' + identifier = a_domain + else: + verify_type = 'SITE' + identifier = f'http://{a_domain}/' + body = {'site':{'type':verify_type, 'identifier':identifier}, 'verificationMethod':verificationMethod} + try: + verify_result = gapi.call(verif.webResource(), 'insert', throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST], verificationMethod=verificationMethod, body=body) + except gapi_errors.GapiBadRequestError as e: + print(f'ERROR: {str(e)}') + verify_data = gapi.call(verif.webResource(), 'getToken', body=body) + print(f'Method: {verify_data["method"]}') + print(f'Expected Token: {verify_data["token"]}') + if verify_data['method'] in ['DNS_CNAME', 'DNS_TXT']: + simplehttp = transport.create_http() + base_url = 'https://dns.google/resolve?' + query_params = {} + if verify_data['method'] == 'DNS_CNAME': + cname_token = verify_data['token'] + cname_list = cname_token.split(' ') + cname_subdomain = cname_list[0] + query_params['name'] = f'{cname_subdomain}.{a_domain}' + query_params['type'] = 'cname' + else: + query_params['name'] = a_domain + query_params['type'] = 'txt' + full_url = base_url + urlencode(query_params) + (_, c) = simplehttp.request(full_url, 'GET') + result = json.loads(c) + status = result['Status'] + if status == 0 and 'Answer' in result: + answers = result['Answer'] + if verify_data['method'] == 'DNS_CNAME': + answer = answers[0]['data'] + else: + answer = 'no matching record found' + for possible_answer in answers: + possible_answer['data'] = possible_answer['data'].strip('"') + if possible_answer['data'].startswith('google-site-verification'): + answer = possible_answer['data'] + break + print(f'Unrelated TXT record: {possible_answer["data"]}') + print(f'Found DNS Record: {answer}') + elif status == 0: + controlflow.system_error_exit(1, 'DNS record not found') + else: + controlflow.system_error_exit(status, DNS_ERROR_CODES_MAP.get(status, f'Unknown error {status}')) + return + print('SUCCESS!') + print(f'Verified: {verify_result["site"]["identifier"]}') + print(f'ID: {verify_result["id"]}') + print(f'Type: {verify_result["site"]["type"]}') + print('All Owners:') + try: + for owner in verify_result['owners']: + print(f' {owner}') + except KeyError: + pass + print() + print(f'You can now add {a_domain} or it\'s subdomains as secondary or domain aliases of the {GC_Values[GC_DOMAIN]} G Suite Account.') + +def orgUnitPathQuery(path, checkSuspended): + query = "orgUnitPath='{0}'".format(path.replace("'", "\\'")) if path != '/' else '' + if checkSuspended is not None: + query += f' isSuspended={checkSuspended}' + return query + +def makeOrgUnitPathAbsolute(path): + if path == '/': + return path + if path.startswith('/'): + return path.rstrip('/') + if path.startswith('id:'): + return path + if path.startswith('uid:'): + return path[1:] + return '/'+path.rstrip('/') + +def makeOrgUnitPathRelative(path): + if path == '/': + return path + if path.startswith('/'): + return path[1:].rstrip('/') + if path.startswith('id:'): + return path + if path.startswith('uid:'): + return path[1:] + return path.rstrip('/') + +def encodeOrgUnitPath(path): + if path.find('+') == -1 and path.find('%') == -1: + return path + encpath = '' + for c in path: + if c == '+': + encpath += '%2B' + elif c == '%': + encpath += '%25' + else: + encpath += c + return encpath + +def getOrgUnitItem(orgUnit, pathOnly=False, absolutePath=True): + if pathOnly and (orgUnit.startswith('id:') or orgUnit.startswith('uid:')): + controlflow.system_error_exit(2, f'{orgUnit} is not valid in this context') + if absolutePath: + return makeOrgUnitPathAbsolute(orgUnit) + return makeOrgUnitPathRelative(orgUnit) + +def getTopLevelOrgId(cd, orgUnitPath): + try: + # create a temp org so we can learn what the top level org ID is (sigh) + temp_org = gapi.call(cd.orgunits(), 'insert', customerId=GC_Values[GC_CUSTOMER_ID], + body={'name': 'temp-delete-me', 'parentOrgUnitPath': orgUnitPath}, + fields='parentOrgUnitId,orgUnitId') + gapi.call(cd.orgunits(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=temp_org['orgUnitId']) + return temp_org['parentOrgUnitId'] + except: + pass + return None + +def getOrgUnitId(orgUnit, cd=None): + if cd is None: + cd = buildGAPIObject('directory') + orgUnit = getOrgUnitItem(orgUnit) + if orgUnit[:3] == 'id:': + return (orgUnit, orgUnit) + if orgUnit == '/': + result = gapi.call(cd.orgunits(), 'list', + customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath='/', type='children', + fields='organizationUnits(parentOrgUnitId)') + if result.get('organizationUnits', []): + return (orgUnit, result['organizationUnits'][0]['parentOrgUnitId']) + topLevelOrgId = getTopLevelOrgId(cd, '/') + if topLevelOrgId: + return (orgUnit, topLevelOrgId) + return (orgUnit, '/') #Bogus but should never happen + result = gapi.call(cd.orgunits(), 'get', + customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnit)), fields='orgUnitId') + return (orgUnit, result['orgUnitId']) + +def doGetOrgInfo(name=None, return_attrib=None): + cd = buildGAPIObject('directory') + checkSuspended = None + if not name: + name = getOrgUnitItem(sys.argv[3]) + get_users = True + show_children = False + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'nousers': + get_users = False + i += 1 + elif myarg in ['children', 'child']: + show_children = True + i += 1 + elif myarg in ['suspended', 'notsuspended']: + checkSuspended = myarg == 'suspended' + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam info org") + if name == '/': + orgs = gapi.call(cd.orgunits(), 'list', + customerId=GC_Values[GC_CUSTOMER_ID], type='children', + fields='organizationUnits/parentOrgUnitId') + if 'organizationUnits' in orgs and orgs['organizationUnits']: + name = orgs['organizationUnits'][0]['parentOrgUnitId'] + else: + topLevelOrgId = getTopLevelOrgId(cd, '/') + if topLevelOrgId: + name = topLevelOrgId + else: + name = makeOrgUnitPathRelative(name) + result = gapi.call(cd.orgunits(), 'get', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(name)) + if return_attrib: + return result[return_attrib] + display.print_json(result) + if get_users: + name = result['orgUnitPath'] + page_message = gapi.got_total_items_first_last_msg('Users') + users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + message_attribute='primaryEmail', customer=GC_Values[GC_CUSTOMER_ID], query=orgUnitPathQuery(name, checkSuspended), + fields='users(primaryEmail,orgUnitPath),nextPageToken') + if checkSuspended is None: + print('Users:') + elif not checkSuspended: + print('Users (Not suspended):') + else: + print('Users (Suspended):') + for user in users: + if show_children or (name.lower() == user['orgUnitPath'].lower()): + sys.stdout.write(f' {user["primaryEmail"]}') + if name.lower() != user['orgUnitPath'].lower(): + print(' (child)') + else: + print('') + +def doGetASPs(users): + cd = buildGAPIObject('directory') + for user in users: + asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user) + if asps: + print(f'Application-Specific Passwords for {user}') + for asp in asps: + if asp['creationTime'] == '0': + created_date = 'Unknown' + else: + created_date = utils.formatTimestampYMDHMS(asp['creationTime']) + if asp['lastTimeUsed'] == '0': + used_date = 'Never' + else: + used_date = utils.formatTimestampYMDHMS(asp['lastTimeUsed']) + print(f' ID: {asp["codeId"]}\n Name: {asp["name"]}\n Created: {created_date}\n Last Used: {used_date}\n') + else: + print(f' no ASPs for {user}\n') + +def doDelASP(users): + cd = buildGAPIObject('directory') + codeIdList = sys.argv[5].lower() + if codeIdList == 'all': + allCodeIds = True + else: + allCodeIds = False + codeIds = codeIdList.replace(',', ' ').split() + for user in users: + if allCodeIds: + asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user, fields='items/codeId') + codeIds = [asp['codeId'] for asp in asps] + for codeId in codeIds: + gapi.call(cd.asps(), 'delete', userKey=user, codeId=codeId) + print(f'deleted ASP {codeId} for {user}') + +def printBackupCodes(user, codes): + jcount = len(codes) + realcount = 0 + for code in codes: + if 'verificationCode' in code and code['verificationCode']: + realcount += 1 + print(f'Backup verification codes for {user} ({realcount})') + print('') + if jcount > 0: + j = 0 + for code in codes: + j += 1 + print(f'{j}. {code["verificationCode"]}') + print('') + +def doGetBackupCodes(users): + cd = buildGAPIObject('directory') + for user in users: + try: + codes = gapi.get_items(cd.verificationCodes(), 'list', 'items', throw_reasons=[gapi_errors.ErrorReason.INVALID_ARGUMENT, gapi_errors.ErrorReason.INVALID], userKey=user) + except (gapi_errors.GapiInvalidArgumentError, gapi_errors.GapiInvalidError): + codes = [] + printBackupCodes(user, codes) + +def doGenBackupCodes(users): + cd = buildGAPIObject('directory') + for user in users: + gapi.call(cd.verificationCodes(), 'generate', userKey=user) + codes = gapi.get_items(cd.verificationCodes(), 'list', 'items', userKey=user) + printBackupCodes(user, codes) + +def doDelBackupCodes(users): + cd = buildGAPIObject('directory') + for user in users: + try: + gapi.call(cd.verificationCodes(), 'invalidate', soft_errors=True, throw_reasons=[gapi_errors.ErrorReason.INVALID], userKey=user) + except gapi_errors.GapiInvalidError: + print(f'No 2SV backup codes for {user}') + continue + print(f'2SV backup codes for {user} invalidated') + +def commonClientIds(clientId): + if clientId == 'gasmo': + return '1095133494869.apps.googleusercontent.com' + return clientId + +def doDelTokens(users): + cd = buildGAPIObject('directory') + clientId = None + i = 5 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'clientid': + clientId = commonClientIds(sys.argv[i+1]) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam delete token") + if not clientId: + controlflow.system_error_exit(3, 'you must specify a clientid for "gam delete token"') + for user in users: + try: + gapi.call(cd.tokens(), 'get', throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND, gapi_errors.ErrorReason.RESOURCE_NOT_FOUND], userKey=user, clientId=clientId) + except (gapi_errors.GapiNotFoundError, gapi_errors.GapiResourceNotFoundError): + print(f'User {user} did not authorize {clientId}') + continue + gapi.call(cd.tokens(), 'delete', userKey=user, clientId=clientId) + print(f'Deleted token for {user}') + +def printShowTokens(i, entityType, users, csvFormat): + def _showToken(token): + print(f' Client ID: {token["clientId"]}') + for item in token: + if item not in ['clientId', 'scopes']: + print(f' {item}: {token.get(item, "")}') + item = 'scopes' + print(f' {item}:') + for it in token.get(item, []): + print(f' {it}') + + cd = buildGAPIObject('directory') + if csvFormat: + todrive = False + titles = ['user', 'clientId', 'displayText', 'anonymous', 'nativeApp', 'userKey', 'scopes'] + csvRows = [] + clientId = None + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if csvFormat and myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'clientid': + clientId = commonClientIds(sys.argv[i+1]) + i += 2 + elif not entityType: + entityType = myarg + users = getUsersToModify(entity_type=entityType, entity=sys.argv[i+1], silent=False) + i += 2 + else: + controlflow.invalid_argument_exit(myarg, f"gam {['show', 'print'][csvFormat]} tokens") + if not entityType: + users = getUsersToModify(entity_type='all', entity='users', silent=False) + fields = ','.join(['clientId', 'displayText', 'anonymous', 'nativeApp', 'userKey', 'scopes']) + i = 0 + count = len(users) + for user in users: + i += 1 + try: + if csvFormat: + sys.stderr.write(f'Getting Access Tokens for {user}\n') + if clientId: + results = [gapi.call(cd.tokens(), 'get', + throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND, gapi_errors.ErrorReason.USER_NOT_FOUND, gapi_errors.ErrorReason.RESOURCE_NOT_FOUND], + userKey=user, clientId=clientId, fields=fields)] + else: + results = gapi.get_items(cd.tokens(), 'list', 'items', + throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND], + userKey=user, fields=f'items({fields})') + jcount = len(results) + if not csvFormat: + print(f'User: {user}, Access Tokens{currentCount(i, count)}') + if jcount == 0: + continue + for token in results: + _showToken(token) + else: + if jcount == 0: + continue + for token in results: + row = {'user': user, 'scopes': ' '.join(token.get('scopes', []))} + for item in token: + if item not in ['scopes']: + row[item] = token.get(item, '') + csvRows.append(row) + except (gapi_errors.GapiNotFoundError, gapi_errors.GapiUserNotFoundError, gapi_errors.GapiResourceNotFoundError): + pass + if csvFormat: + display.write_csv_file(csvRows, titles, 'OAuth Tokens', todrive) + +def doDeprovUser(users): + cd = buildGAPIObject('directory') + for user in users: + print(f'Getting Application Specific Passwords for {user}') + asps = gapi.get_items(cd.asps(), 'list', 'items', userKey=user, fields='items/codeId') + jcount = len(asps) + if jcount > 0: + j = 0 + for asp in asps: + j += 1 + print(f' deleting ASP {j} of {jcount}') + gapi.call(cd.asps(), 'delete', userKey=user, codeId=asp['codeId']) + else: + print('No ASPs') + print(f'Invalidating 2SV Backup Codes for {user}') + try: + gapi.call(cd.verificationCodes(), 'invalidate', soft_errors=True, throw_reasons=[gapi_errors.ErrorReason.INVALID], userKey=user) + except gapi_errors.GapiInvalidError: + print('No 2SV Backup Codes') + print(f'Getting tokens for {user}...') + tokens = gapi.get_items(cd.tokens(), 'list', 'items', userKey=user, fields='items/clientId') + jcount = len(tokens) + if jcount > 0: + j = 0 + for token in tokens: + j += 1 + print(f' deleting token {j} of {jcount})') + gapi.call(cd.tokens(), 'delete', userKey=user, clientId=token['clientId']) + else: + print('No Tokens') + print(f'Done deprovisioning {user}') + +def doDeleteUser(): + cd = buildGAPIObject('directory') + user_email = normalizeEmailAddressOrUID(sys.argv[3]) + print(f'Deleting account for {user_email}') + gapi.call(cd.users(), 'delete', userKey=user_email) + +def doUndeleteUser(): + cd = buildGAPIObject('directory') + user = normalizeEmailAddressOrUID(sys.argv[3]) + orgUnit = '/' + i = 4 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg in ['ou', 'org']: + orgUnit = makeOrgUnitPathAbsolute(sys.argv[i+1]) + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam undelete user") + if user.find('@') == -1: + user_uid = user + else: + print(f'Looking up UID for {user}...') + deleted_users = gapi.get_all_pages(cd.users(), 'list', 'users', + customer=GC_Values[GC_CUSTOMER_ID], showDeleted=True) + matching_users = list() + for deleted_user in deleted_users: + if str(deleted_user['primaryEmail']).lower() == user: + matching_users.append(deleted_user) + if not matching_users: + controlflow.system_error_exit(3, 'could not find deleted user with that address.') + elif len(matching_users) > 1: + print(f'ERROR: more than one matching deleted {user} user. Please select the correct one to undelete and specify with "gam undelete user uid:"') + print('') + for matching_user in matching_users: + print(f' uid:{matching_user["id"]} ') + for attr_name in ['creationTime', 'lastLoginTime', 'deletionTime']: + try: + if matching_user[attr_name] == NEVER_TIME: + matching_user[attr_name] = 'Never' + print(f' {attr_name}: {matching_user[attr_name]} ') + except KeyError: + pass + print() + sys.exit(3) + else: + user_uid = matching_users[0]['id'] + print(f'Undeleting account for {user}') + gapi.call(cd.users(), 'undelete', userKey=user_uid, body={'orgUnitPath': orgUnit}) + +def doDeleteGroup(): + cd = buildGAPIObject('directory') + group = normalizeEmailAddressOrUID(sys.argv[3]) + print(f'Deleting group {group}') + gapi.call(cd.groups(), 'delete', groupKey=group) + +def doDeleteAlias(alias_email=None): + cd = buildGAPIObject('directory') + is_user = is_group = False + if alias_email is None: + alias_email = sys.argv[3] + if alias_email.lower() == 'user': + is_user = True + alias_email = sys.argv[4] + elif alias_email.lower() == 'group': + is_group = True + alias_email = sys.argv[4] + alias_email = normalizeEmailAddressOrUID(alias_email, noUid=True, noLower=True) + print(f'Deleting alias {alias_email}') + if is_user or (not is_user and not is_group): + try: + gapi.call(cd.users().aliases(), 'delete', throw_reasons=[gapi_errors.ErrorReason.INVALID, gapi_errors.ErrorReason.BAD_REQUEST, gapi_errors.ErrorReason.NOT_FOUND], userKey=alias_email, alias=alias_email) + return + except (gapi_errors.GapiInvalidError, gapi_errors.GapiBadRequestError): + pass + except gapi_errors.GapiNotFoundError: + controlflow.system_error_exit(4, f'The alias {alias_email} does not exist') + if not is_user or (not is_user and not is_group): + gapi.call(cd.groups().aliases(), 'delete', groupKey=alias_email, alias=alias_email) + +def doDeleteOrg(): + cd = buildGAPIObject('directory') + name = getOrgUnitItem(sys.argv[3]) + print(f'Deleting organization {name}') + gapi.call(cd.orgunits(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(name))) + +def send_email(subject, body, recipient=None, sender=None, user=None, method='send', labels=None, msgHeaders={}, kwargs={}): + api_body = {} + default_sender = default_recipient = False + if not user: + user = _getValueFromOAuth('email') + userId, gmail = buildGmailGAPIObject(user) + if not gmail: + return + resource = gmail.users().messages() + if labels: + api_body['labelIds'] = labelsToLabelIds(gmail, labels) + if not sender: + sender = userId + default_sender = True + if not recipient: + recipient = userId + default_recipient = True + # Force ASCII for RFC compliance + # xmlcharref seems to work to display at least + # some unicode in HTML body and is ignored in + # plain text body. + body = body.encode('ascii', 'xmlcharrefreplace').decode() + msg = message_from_string(body) + for header, value in msgHeaders.items(): + msg.__delitem__(header) # can remove multiple case-insensitive matching headers + msg.add_header(header, value) + if subject: + msg.__delitem__('Subject') + msg['Subject'] = subject + if not default_sender: + msg.__delitem__('From') + if not msg['From']: + msg['From'] = sender + if not default_recipient: + msg.__delitem__('to') + if not msg['To']: + msg['To'] = recipient + api_body['raw'] = base64.urlsafe_b64encode(msg.as_bytes()).decode() + if method == 'draft': + resource = gmail.users().drafts() + method = 'create' + api_body = {'message': api_body} + elif method in ['insert', 'import']: + if method == 'import': + method = 'import_' + gapi.call(resource, method, userId=userId, body=api_body, **kwargs) + +USER_ARGUMENT_TO_PROPERTY_MAP = { + 'address': ['addresses',], + 'addresses': ['addresses',], + 'admin': ['isAdmin', 'isDelegatedAdmin',], + 'agreed2terms': ['agreedToTerms',], + 'agreedtoterms': ['agreedToTerms',], + 'aliases': ['aliases', 'nonEditableAliases',], + 'archived': ['archived',], + 'changepassword': ['changePasswordAtNextLogin',], + 'changepasswordatnextlogin': ['changePasswordAtNextLogin',], + 'creationtime': ['creationTime',], + 'deletiontime': ['deletionTime',], + 'email': ['emails',], + 'emails': ['emails',], + 'externalid': ['externalIds',], + 'externalids': ['externalIds',], + 'familyname': ['name.familyName',], + 'firstname': ['name.givenName',], + 'fullname': ['name.fullName',], + 'gal': ['includeInGlobalAddressList',], + 'gender': ['gender.type', 'gender.customGender', 'gender.addressMeAs',], + 'givenname': ['name.givenName',], + 'id': ['id',], + 'im': ['ims',], + 'ims': ['ims',], + 'includeinglobaladdresslist': ['includeInGlobalAddressList',], + 'ipwhitelisted': ['ipWhitelisted',], + 'isadmin': ['isAdmin', 'isDelegatedAdmin',], + 'isdelegatedadmin': ['isAdmin', 'isDelegatedAdmin',], + 'isenforcedin2sv': ['isEnforcedIn2Sv',], + 'isenrolledin2sv': ['isEnrolledIn2Sv',], + 'is2svenforced': ['isEnforcedIn2Sv',], + 'is2svenrolled': ['isEnrolledIn2Sv',], + 'ismailboxsetup': ['isMailboxSetup',], + 'keyword': ['keywords',], + 'keywords': ['keywords',], + 'language': ['languages',], + 'languages': ['languages',], + 'lastlogintime': ['lastLoginTime',], + 'lastname': ['name.familyName',], + 'location': ['locations',], + 'locations': ['locations',], + 'name': ['name.givenName', 'name.familyName', 'name.fullName',], + 'nicknames': ['aliases', 'nonEditableAliases',], + 'noneditablealiases': ['aliases', 'nonEditableAliases',], + 'note': ['notes',], + 'notes': ['notes',], + 'org': ['orgUnitPath',], + 'organization': ['organizations',], + 'organizations': ['organizations',], + 'orgunitpath': ['orgUnitPath',], + 'otheremail': ['emails',], + 'otheremails': ['emails',], + 'ou': ['orgUnitPath',], + 'phone': ['phones',], + 'phones': ['phones',], + 'photo': ['thumbnailPhotoUrl',], + 'photourl': ['thumbnailPhotoUrl',], + 'posix': ['posixAccounts',], + 'posixaccounts': ['posixAccounts',], + 'primaryemail': ['primaryEmail',], + 'recoveryemail': ['recoveryEmail',], + 'recoveryphone': ['recoveryPhone',], + 'relation': ['relations',], + 'relations': ['relations',], + 'ssh': ['sshPublicKeys',], + 'sshkeys': ['sshPublicKeys',], + 'sshpublickeys': ['sshPublicKeys',], + 'suspended': ['suspended', 'suspensionReason',], + 'thumbnailphotourl': ['thumbnailPhotoUrl',], + 'username': ['primaryEmail',], + 'website': ['websites',], + 'websites': ['websites',], + } + +def doPrintUsers(): + cd = buildGAPIObject('directory') + todrive = False + fieldsList = [] + fieldsTitles = {} + titles = [] + csvRows = [] + display.add_field_to_csv_file('primaryemail', USER_ARGUMENT_TO_PROPERTY_MAP, fieldsList, fieldsTitles, titles) + customer = GC_Values[GC_CUSTOMER_ID] + domain = None + queries = [None] + projection = 'basic' + customFieldMask = None + sortHeaders = getGroupFeed = getLicenseFeed = email_parts = False + viewType = deleted_only = orderBy = sortOrder = None + groupDelimiter = ' ' + licenseDelimiter = ',' + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg in PROJECTION_CHOICES_MAP: + projection = myarg + sortHeaders = True + fieldsList = [] + i += 1 + elif myarg == 'allfields': + projection = 'basic' + sortHeaders = True + fieldsList = [] + i += 1 + elif myarg == 'delimiter': + groupDelimiter = licenseDelimiter = sys.argv[i+1] + i += 2 + elif myarg == 'sortheaders': + sortHeaders = True + i += 1 + elif myarg in ['custom', 'schemas']: + fieldsList.append('customSchemas') + if sys.argv[i+1].lower() == 'all': + projection = 'full' + else: + projection = 'custom' + customFieldMask = sys.argv[i+1] + i += 2 + elif myarg == 'todrive': + todrive = True + i += 1 + elif myarg in ['deletedonly', 'onlydeleted']: + deleted_only = True + i += 1 + elif myarg == 'orderby': + orderBy = sys.argv[i+1] + validOrderBy = ['email', 'familyname', 'givenname', 'firstname', 'lastname'] + if orderBy.lower() not in validOrderBy: + controlflow.expected_argument_exit("orderby", ", ".join(validOrderBy), orderBy) + if orderBy.lower() in ['familyname', 'lastname']: + orderBy = 'familyName' + elif orderBy.lower() in ['givenname', 'firstname']: + orderBy = 'givenName' + i += 2 + elif myarg == 'userview': + viewType = 'domain_public' + i += 1 + elif myarg in SORTORDER_CHOICES_MAP: + sortOrder = SORTORDER_CHOICES_MAP[myarg] + i += 1 + elif myarg == 'domain': + domain = sys.argv[i+1] + customer = None + i += 2 + elif myarg in ['query', 'queries']: + queries = getQueries(myarg, sys.argv[i+1]) + i += 2 + elif myarg in USER_ARGUMENT_TO_PROPERTY_MAP: + if not fieldsList: + fieldsList = ['primaryEmail',] + display.add_field_to_csv_file(myarg, USER_ARGUMENT_TO_PROPERTY_MAP, fieldsList, fieldsTitles, titles) + i += 1 + elif myarg == 'fields': + if not fieldsList: + fieldsList = ['primaryEmail',] + fieldNameList = sys.argv[i+1] + for field in fieldNameList.lower().replace(',', ' ').split(): + if field in USER_ARGUMENT_TO_PROPERTY_MAP: + display.add_field_to_csv_file(field, USER_ARGUMENT_TO_PROPERTY_MAP, fieldsList, fieldsTitles, titles) + else: + controlflow.invalid_argument_exit(field, "gam print users fields") + i += 2 + elif myarg == 'groups': + getGroupFeed = True + i += 1 + elif myarg in ['license', 'licenses', 'licence', 'licences']: + getLicenseFeed = True + i += 1 + elif myarg in ['emailpart', 'emailparts', 'username']: + email_parts = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print users") + if fieldsList: + fields = f'nextPageToken,users({",".join(set(fieldsList)).replace(".", "/")})' + else: + fields = None + for query in queries: + printGettingAllItems('Users', query) + page_message = gapi.got_total_items_first_last_msg('Users') + all_users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + message_attribute='primaryEmail', customer=customer, domain=domain, fields=fields, + showDeleted=deleted_only, orderBy=orderBy, sortOrder=sortOrder, viewType=viewType, + query=query, projection=projection, customFieldMask=customFieldMask) + for user in all_users: + if email_parts and ('primaryEmail' in user): + user_email = user['primaryEmail'] + if user_email.find('@') != -1: + user['primaryEmailLocal'], user['primaryEmailDomain'] = splitEmailAddress(user_email) + display.add_row_titles_to_csv_file(utils.flatten_json(user), csvRows, titles) + if sortHeaders: + display.sort_csv_titles(['primaryEmail',], titles) + if getGroupFeed: + i = 0 + count = len(csvRows) + titles.append('Groups') + for user in csvRows: + i += 1 + user_email = user['primaryEmail'] + sys.stderr.write(f'Getting Group Membership for {user_email}{currentCountNL(i, count)}') + groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', userKey=user_email) + user['Groups'] = groupDelimiter.join([groupname['email'] for groupname in groups]) + if getLicenseFeed: + titles.append('Licenses') + licenses = doPrintLicenses(returnFields='userId,skuId') + if licenses: + for user in csvRows: + u_licenses = licenses.get(user['primaryEmail'].lower()) + if u_licenses: + user['Licenses'] = licenseDelimiter.join([_skuIdToDisplayName(skuId) for skuId in u_licenses]) + display.write_csv_file(csvRows, titles, 'Users', todrive) + +def doPrintShowAlerts(): + _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) + alerts = gapi.get_all_pages(ac.alerts(), 'list', 'alerts') + titles = [] + csv_rows = [] + for alert in alerts: + aj = utils.flatten_json(alert) + for field in aj: + if field not in titles: + titles.append(field) + csv_rows.append(aj) + display.write_csv_file(csv_rows, titles, 'Alerts', False) + +def doPrintShowAlertFeedback(): + _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) + feedback = gapi.get_all_pages(ac.alerts().feedback(), 'list', 'feedback', alertId='-') + for feedbac in feedback: + print(feedbac) + +def doCreateAlertFeedback(): + _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) + valid_types = gapi.get_enum_values_minus_unspecified(ac._rootDesc['schemas']['AlertFeedback']['properties']['type']['enum']) + alertId = sys.argv[3] + body = {'type': sys.argv[4].upper()} + if body['type'] not in valid_types: + controlflow.system_error_exit(2, f'{body["type"]} is not a valid feedback value, expected one of: {", ".join(valid_types)}') + gapi.call(ac.alerts().feedback(), 'create', alertId=alertId, body=body) + +def doDeleteOrUndeleteAlert(action): + _, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email')) + alertId = sys.argv[3] + kwargs = {} + if action == 'undelete': + kwargs['body'] = {} + gapi.call(ac.alerts(), action, alertId=alertId, **kwargs) + +GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP = { + 'admincreated': ['adminCreated', 'Admin_Created'], + 'aliases': ['aliases', 'Aliases', 'nonEditableAliases', 'NonEditableAliases'], + 'description': ['description', 'Description'], + 'directmemberscount': ['directMembersCount', 'DirectMembersCount'], + 'email': ['email', 'Email'], + 'id': ['id', 'ID'], + 'name': ['name', 'Name'], + } + +GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = { + 'allowexternalmembers': 'allowExternalMembers', + 'allowgooglecommunication': 'allowGoogleCommunication', + 'allowwebposting': 'allowWebPosting', + 'archiveonly': 'archiveOnly', + 'customfootertext': 'customFooterText', + 'customreplyto': 'customReplyTo', + 'defaultmessagedenynotificationtext': 'defaultMessageDenyNotificationText', + 'enablecollaborativeinbox': 'enableCollaborativeInbox', + 'favoriterepliesontop': 'favoriteRepliesOnTop', + 'gal': 'includeInGlobalAddressList', + 'includecustomfooter': 'includeCustomFooter', + 'includeinglobaladdresslist': 'includeInGlobalAddressList', + 'isarchived': 'isArchived', + 'memberscanpostasthegroup': 'membersCanPostAsTheGroup', + 'messagemoderationlevel': 'messageModerationLevel', + 'primarylanguage': 'primaryLanguage', + 'replyto': 'replyTo', + 'sendmessagedenynotification': 'sendMessageDenyNotification', + 'showingroupdirectory': 'showInGroupDirectory', + 'spammoderationlevel': 'spamModerationLevel', + 'whocanadd': 'whoCanAdd', + 'whocanapprovemembers': 'whoCanApproveMembers', + 'whocanapprovemessages': 'whoCanApproveMessages', + 'whocanassigntopics': 'whoCanAssignTopics', + 'whocanassistcontent': 'whoCanAssistContent', + 'whocanbanusers': 'whoCanBanUsers', + 'whocancontactowner': 'whoCanContactOwner', + 'whocandeleteanypost': 'whoCanDeleteAnyPost', + 'whocandeletetopics': 'whoCanDeleteTopics', + 'whocandiscovergroup': 'whoCanDiscoverGroup', + 'whocanenterfreeformtags': 'whoCanEnterFreeFormTags', + 'whocanhideabuse': 'whoCanHideAbuse', + 'whocaninvite': 'whoCanInvite', + 'whocanjoin': 'whoCanJoin', + 'whocanleavegroup': 'whoCanLeaveGroup', + 'whocanlocktopics': 'whoCanLockTopics', + 'whocanmaketopicssticky': 'whoCanMakeTopicsSticky', + 'whocanmarkduplicate': 'whoCanMarkDuplicate', + 'whocanmarkfavoritereplyonanytopic': 'whoCanMarkFavoriteReplyOnAnyTopic', + 'whocanmarkfavoritereplyonowntopic': 'whoCanMarkFavoriteReplyOnOwnTopic', + 'whocanmarknoresponseneeded': 'whoCanMarkNoResponseNeeded', + 'whocanmoderatecontent': 'whoCanModerateContent', + 'whocanmoderatemembers': 'whoCanModerateMembers', + 'whocanmodifymembers': 'whoCanModifyMembers', + 'whocanmodifytagsandcategories': 'whoCanModifyTagsAndCategories', + 'whocanmovetopicsin': 'whoCanMoveTopicsIn', + 'whocanmovetopicsout': 'whoCanMoveTopicsOut', + 'whocanpostannouncements': 'whoCanPostAnnouncements', + 'whocanpostmessage': 'whoCanPostMessage', + 'whocantaketopics': 'whoCanTakeTopics', + 'whocanunassigntopic': 'whoCanUnassignTopic', + 'whocanunmarkfavoritereplyonanytopic': 'whoCanUnmarkFavoriteReplyOnAnyTopic', + 'whocanviewgroup': 'whoCanViewGroup', + 'whocanviewmembership': 'whoCanViewMembership', + } + +def doPrintGroups(): + cd = buildGAPIObject('directory') + i = 3 + members = membersCountOnly = managers = managersCountOnly = owners = ownersCountOnly = False + customer = GC_Values[GC_CUSTOMER_ID] + usedomain = usemember = usequery = None + aliasDelimiter = ' ' + memberDelimiter = '\n' + todrive = False + cdfieldsList = [] + gsfieldsList = [] + fieldsTitles = {} + titles = [] + csvRows = [] + display.add_field_title_to_csv_file('email', GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) + roles = [] + getSettings = sortHeaders = False + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'domain': + usedomain = sys.argv[i+1].lower() + customer = None + i += 2 + elif myarg == 'member': + usemember = normalizeEmailAddressOrUID(sys.argv[i+1]) + customer = usequery = None + i += 2 + elif myarg == 'query': + usequery = sys.argv[i+1] + usemember = None + i += 2 + elif myarg == 'maxresults': + # deprecated argument + i += 2 + elif myarg == 'delimiter': + aliasDelimiter = memberDelimiter = sys.argv[i+1] + i += 2 + elif myarg in GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP: + display.add_field_title_to_csv_file(myarg, GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) + i += 1 + elif myarg == 'settings': + getSettings = True + i += 1 + elif myarg == 'allfields': + getSettings = sortHeaders = True + cdfieldsList = [] + gsfieldsList = [] + fieldsTitles = {} + for field in GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP: + display.add_field_title_to_csv_file(field, GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) + i += 1 + elif myarg == 'sortheaders': + sortHeaders = True + i += 1 + elif myarg == 'fields': + fieldNameList = sys.argv[i+1] + for field in fieldNameList.lower().replace(',', ' ').split(): + if field in GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP: + display.add_field_title_to_csv_file(field, GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP, cdfieldsList, fieldsTitles, titles) + elif field in GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP: + display.add_field_to_csv_file(field, {field: [GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP[field]]}, gsfieldsList, fieldsTitles, titles) + elif field == 'collaborative': + for attrName in COLLABORATIVE_INBOX_ATTRIBUTES: + display.add_field_to_csv_file(attrName, {attrName: [attrName]}, gsfieldsList, fieldsTitles, titles) + else: + controlflow.invalid_argument_exit(field, "gam print groups fields") + i += 2 + elif myarg in ['members', 'memberscount']: + roles.append(ROLE_MEMBER) + members = True + if myarg == 'memberscount': + membersCountOnly = True + i += 1 + elif myarg in ['owners', 'ownerscount']: + roles.append(ROLE_OWNER) + owners = True + if myarg == 'ownerscount': + ownersCountOnly = True + i += 1 + elif myarg in ['managers', 'managerscount']: + roles.append(ROLE_MANAGER) + managers = True + if myarg == 'managerscount': + managersCountOnly = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print groups") + cdfields = ','.join(set(cdfieldsList)) + if gsfieldsList: + getSettings = True + gsfields = ','.join(set(gsfieldsList)) + elif getSettings: + gsfields = None + if getSettings: + gs = buildGAPIObject('groupssettings') + roles = ','.join(sorted(set(roles))) + if roles: + if members: + display.add_titles_to_csv_file(['MembersCount',], titles) + if not membersCountOnly: + display.add_titles_to_csv_file(['Members',], titles) + if managers: + display.add_titles_to_csv_file(['ManagersCount',], titles) + if not managersCountOnly: + display.add_titles_to_csv_file(['Managers',], titles) + if owners: + display.add_titles_to_csv_file(['OwnersCount',], titles) + if not ownersCountOnly: + display.add_titles_to_csv_file(['Owners',], titles) + printGettingAllItems('Groups', None) + page_message = gapi.got_total_items_first_last_msg('Groups') + entityList = gapi.get_all_pages(cd.groups(), 'list', 'groups', + page_message=page_message, message_attribute='email', + customer=customer, domain=usedomain, userKey=usemember, query=usequery, + fields=f'nextPageToken,groups({cdfields})') + i = 0 + count = len(entityList) + for groupEntity in entityList: + i += 1 + groupEmail = groupEntity['email'] + group = {} + for field in cdfieldsList: + if field in groupEntity: + if isinstance(groupEntity[field], list): + group[fieldsTitles[field]] = aliasDelimiter.join(groupEntity[field]) + else: + group[fieldsTitles[field]] = groupEntity[field] + if roles: + sys.stderr.write(f' Getting {roles} for {groupEmail}{currentCountNL(i, count)}') + page_message = gapi.got_total_items_first_last_msg('Members') + validRoles, listRoles, listFields = _getRoleVerification(roles, 'nextPageToken,members(email,id,role)') + groupMembers = gapi.get_all_pages(cd.members(), 'list', 'members', + page_message=page_message, message_attribute='email', + soft_errors=True, + groupKey=groupEmail, roles=listRoles, fields=listFields) + if members: + membersList = [] + membersCount = 0 + if managers: + managersList = [] + managersCount = 0 + if owners: + ownersList = [] + ownersCount = 0 + for member in groupMembers: + member_email = member.get('email', member.get('id', None)) + if not member_email: + sys.stderr.write(f' Not sure what to do with: {member}') + continue + role = member.get('role', ROLE_MEMBER) + if not validRoles or role in validRoles: + if role == ROLE_MEMBER: + if members: + membersCount += 1 + if not membersCountOnly: + membersList.append(member_email) + elif role == ROLE_MANAGER: + if managers: + managersCount += 1 + if not managersCountOnly: + managersList.append(member_email) + elif role == ROLE_OWNER: + if owners: + ownersCount += 1 + if not ownersCountOnly: + ownersList.append(member_email) + elif members: + membersCount += 1 + if not membersCountOnly: + membersList.append(member_email) + if members: + group['MembersCount'] = membersCount + if not membersCountOnly: + group['Members'] = memberDelimiter.join(membersList) + if managers: + group['ManagersCount'] = managersCount + if not managersCountOnly: + group['Managers'] = memberDelimiter.join(managersList) + if owners: + group['OwnersCount'] = ownersCount + if not ownersCountOnly: + group['Owners'] = memberDelimiter.join(ownersList) + if getSettings and not GroupIsAbuseOrPostmaster(groupEmail): + sys.stderr.write(f' Retrieving Settings for group {groupEmail}{currentCountNL(i, count)}') + settings = gapi.call(gs.groups(), 'get', + soft_errors=True, + retry_reasons=[gapi_errors.ErrorReason.SERVICE_LIMIT, gapi_errors.ErrorReason.INVALID], + groupUniqueId=groupEmail, fields=gsfields) + if settings: + for key in settings: + if key in ['email', 'name', 'description', 'kind', 'etag']: + continue + setting_value = settings[key] + if setting_value is None: + setting_value = '' + if key not in titles: + titles.append(key) + group[key] = setting_value + else: + sys.stderr.write(f" Settings unavailable for group {groupEmail}{currentCountNL(i, count)}") + csvRows.append(group) + if sortHeaders: + display.sort_csv_titles(['Email',], titles) + display.write_csv_file(csvRows, titles, 'Groups', todrive) + +def doPrintOrgs(): + print_order = ['orgUnitPath', 'orgUnitId', 'name', 'description', + 'parentOrgUnitPath', 'parentOrgUnitId', 'blockInheritance'] + cd = buildGAPIObject('directory') + listType = 'all' + orgUnitPath = "/" + todrive = False + fields = ['orgUnitPath', 'name', 'orgUnitId', 'parentOrgUnitId'] + titles = [] + csvRows = [] + parentOrgIds = [] + retrievedOrgIds = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'toplevelonly': + listType = 'children' + i += 1 + elif myarg == 'fromparent': + orgUnitPath = getOrgUnitItem(sys.argv[i+1]) + i += 2 + elif myarg == 'allfields': + fields = None + i += 1 + elif myarg == 'fields': + fields += sys.argv[i+1].split(',') + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print orgs") + printGettingAllItems('Organizational Units', None) + if fields: + get_fields = ','.join(fields) + list_fields = f'organizationUnits({get_fields})' + else: + list_fields = None + get_fields = None + orgs = gapi.call(cd.orgunits(), 'list', + customerId=GC_Values[GC_CUSTOMER_ID], type=listType, orgUnitPath=orgUnitPath, fields=list_fields) + if not 'organizationUnits' in orgs: + topLevelOrgId = getTopLevelOrgId(cd, orgUnitPath) + if topLevelOrgId: + parentOrgIds.append(topLevelOrgId) + orgunits = [] + else: + orgunits = orgs['organizationUnits'] + for row in orgunits: + retrievedOrgIds.append(row['orgUnitId']) + if row['parentOrgUnitId'] not in parentOrgIds: + parentOrgIds.append(row['parentOrgUnitId']) + missing_parents = set(parentOrgIds) - set(retrievedOrgIds) + for missing_parent in missing_parents: + try: + result = gapi.call(cd.orgunits(), 'get', throw_reasons=['required'], + customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=missing_parent, fields=get_fields) + orgunits.append(result) + except: + pass + for row in orgunits: + orgEntity = {} + for key, value in list(row.items()): + if key in ['kind', 'etag', 'etags']: + continue + if key not in titles: + titles.append(key) + orgEntity[key] = value + csvRows.append(orgEntity) + for title in titles: + if title not in print_order: + print_order.append(title) + titles = sorted(titles, key=print_order.index) + # sort results similar to how they list in admin console + csvRows.sort(key=lambda x: x['orgUnitPath'].lower(), reverse=False) + display.write_csv_file(csvRows, titles, 'Orgs', todrive) + +def doPrintAliases(): + cd = buildGAPIObject('directory') + todrive = False + titles = ['Alias', 'Target', 'TargetType'] + csvRows = [] + userFields = ['primaryEmail', 'aliases'] + groupFields = ['email', 'aliases'] + doGroups = doUsers = True + queries = [None] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'shownoneditable': + titles.insert(1, 'NonEditableAlias') + userFields.append('nonEditableAliases') + groupFields.append('nonEditableAliases') + i += 1 + elif myarg == 'nogroups': + doGroups = False + i += 1 + elif myarg == 'nousers': + doUsers = False + i += 1 + elif myarg in ['query', 'queries']: + queries = getQueries(myarg, sys.argv[i+1]) + doGroups = False + doUsers = True + i += 2 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print aliases") + if doUsers: + for query in queries: + printGettingAllItems('User Aliases', query) + page_message = gapi.got_total_items_first_last_msg('Users') + all_users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + message_attribute='primaryEmail', customer=GC_Values[GC_CUSTOMER_ID], query=query, + fields=f'nextPageToken,users({",".join(userFields)})') + for user in all_users: + for alias in user.get('aliases', []): + csvRows.append({'Alias': alias, 'Target': user['primaryEmail'], 'TargetType': 'User'}) + for alias in user.get('nonEditableAliases', []): + csvRows.append({'NonEditableAlias': alias, 'Target': user['primaryEmail'], 'TargetType': 'User'}) + if doGroups: + printGettingAllItems('Group Aliases', None) + page_message = gapi.got_total_items_first_last_msg('Groups') + all_groups = gapi.get_all_pages(cd.groups(), 'list', 'groups', page_message=page_message, + message_attribute='email', customer=GC_Values[GC_CUSTOMER_ID], + fields=f'nextPageToken,groups({",".join(groupFields)})') + for group in all_groups: + for alias in group.get('aliases', []): + csvRows.append({'Alias': alias, 'Target': group['email'], 'TargetType': 'Group'}) + for alias in group.get('nonEditableAliases', []): + csvRows.append({'NonEditableAlias': alias, 'Target': group['email'], 'TargetType': 'Group'}) + display.write_csv_file(csvRows, titles, 'Aliases', todrive) + +def doPrintGroupMembers(): + cd = buildGAPIObject('directory') + todrive = False + membernames = False + includeDerivedMembership = False + customer = GC_Values[GC_CUSTOMER_ID] + checkSuspended = usedomain = usemember = usequery = None + roles = [] + fields = 'nextPageToken,members(email,id,role,status,type)' + titles = ['group'] + csvRows = [] + groups_to_get = [] + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg == 'domain': + usedomain = sys.argv[i+1].lower() + customer = None + i += 2 + elif myarg == 'member': + usemember = normalizeEmailAddressOrUID(sys.argv[i+1]) + customer = usequery = None + i += 2 + elif myarg == 'query': + usequery = sys.argv[i+1] + usemember = None + i += 2 + elif myarg == 'fields': + memberFieldsList = sys.argv[i+1].replace(',', ' ').lower().split() + fields = f'nextPageToken,members({",".join(memberFieldsList)})' + i += 2 + elif myarg == 'membernames': + membernames = True + titles.append('name') + i += 1 + elif myarg in ['role', 'roles']: + for role in sys.argv[i+1].lower().replace(',', ' ').split(): + if role in GROUP_ROLES_MAP: + roles.append(GROUP_ROLES_MAP[role]) + else: + controlflow.system_error_exit(2, f'{role} is not a valid role for "gam print group-members {myarg}"') + i += 2 + elif myarg in ['group', 'groupns', 'groupsusp']: + group_email = normalizeEmailAddressOrUID(sys.argv[i+1]) + groups_to_get = [{'email': group_email}] + if myarg == 'groupns': + checkSuspended = False + elif myarg == 'groupsusp': + checkSuspended = True + i += 2 + elif myarg in ['suspended', 'notsuspended']: + checkSuspended = myarg == 'suspended' + i += 1 + elif myarg == 'includederivedmembership': + includeDerivedMembership = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print group-members") + if not groups_to_get: + groups_to_get = gapi.get_all_pages(cd.groups(), 'list', 'groups', message_attribute='email', + customer=customer, domain=usedomain, userKey=usemember, query=usequery, + fields='nextPageToken,groups(email)') + i = 0 + count = len(groups_to_get) + for group in groups_to_get: + i += 1 + group_email = group['email'] + sys.stderr.write(f'Getting members for {group_email}{currentCountNL(i, count)}') + validRoles, listRoles, listFields = _getRoleVerification(','.join(roles), fields) + group_members = gapi.get_all_pages(cd.members(), 'list', 'members', + soft_errors=True, + includeDerivedMembership=includeDerivedMembership, + groupKey=group_email, roles=listRoles, fields=listFields) + for member in group_members: + if not _checkMemberRoleIsSuspended(member, validRoles, checkSuspended): + continue + for title in member: + if title not in titles: + titles.append(title) + member['group'] = group_email + if membernames and 'type' in member and 'id' in member: + if member['type'] == 'USER': + try: + mbinfo = gapi.call(cd.users(), 'get', + throw_reasons=[gapi_errors.ErrorReason.USER_NOT_FOUND, gapi_errors.ErrorReason.NOT_FOUND, gapi_errors.ErrorReason.FORBIDDEN], + userKey=member['id'], fields='name') + memberName = mbinfo['name']['fullName'] + except (gapi_errors.GapiUserNotFoundError, gapi_errors.GapiNotFoundError, gapi_errors.GapiForbiddenError): + memberName = 'Unknown' + elif member['type'] == 'GROUP': + try: + mbinfo = gapi.call(cd.groups(), 'get', + throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND, gapi_errors.ErrorReason.FORBIDDEN], + groupKey=member['id'], fields='name') + memberName = mbinfo['name'] + except (gapi_errors.GapiNotFoundError, gapi_errors.GapiForbiddenError): + memberName = 'Unknown' + elif member['type'] == 'CUSTOMER': + try: + mbinfo = gapi.call(cd.customers(), 'get', + throw_reasons=[gapi_errors.ErrorReason.BAD_REQUEST, gapi_errors.ErrorReason.RESOURCE_NOT_FOUND, gapi_errors.ErrorReason.FORBIDDEN], + customerKey=member['id'], fields='customerDomain') + memberName = mbinfo['customerDomain'] + except (gapi_errors.GapiBadRequestError, gapi_errors.GapiResourceNotFoundError, gapi_errors.GapiForbiddenError): + memberName = 'Unknown' + else: + memberName = 'Unknown' + member['name'] = memberName + csvRows.append(member) + display.write_csv_file(csvRows, titles, 'Group Members', todrive) + +def doPrintMobileDevices(): + cd = buildGAPIObject('directory') + todrive = False + titles = [] + csvRows = [] + fields = None + projection = orderBy = sortOrder = None + queries = [None] + delimiter = ' ' + listLimit = 1 + appsLimit = -1 + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'todrive': + todrive = True + i += 1 + elif myarg in ['query', 'queries']: + queries = getQueries(myarg, sys.argv[i+1]) + i += 2 + elif myarg == 'delimiter': + delimiter = sys.argv[i+1] + i += 2 + elif myarg == 'listlimit': + listLimit = getInteger(sys.argv[i+1], myarg, minVal=-1) + i += 2 + elif myarg == 'appslimit': + appsLimit = getInteger(sys.argv[i+1], myarg, minVal=-1) + i += 2 + elif myarg == 'fields': + fields = f'nextPageToken,mobiledevices({sys.argv[i+1]})' + i += 2 + elif myarg == 'orderby': + orderBy = sys.argv[i+1].lower() + validOrderBy = ['deviceid', 'email', 'lastsync', 'model', 'name', 'os', 'status', 'type'] + if orderBy not in validOrderBy: + controlflow.expected_argument_exit("orderby", ", ".join(validOrderBy), orderBy) + if orderBy == 'lastsync': + orderBy = 'lastSync' + elif orderBy == 'deviceid': + orderBy = 'deviceId' + i += 2 + elif myarg in SORTORDER_CHOICES_MAP: + sortOrder = SORTORDER_CHOICES_MAP[myarg] + i += 1 + elif myarg in PROJECTION_CHOICES_MAP: + projection = PROJECTION_CHOICES_MAP[myarg] + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print mobile") + for query in queries: + printGettingAllItems('Mobile Devices', query) + page_message = gapi.got_total_items_msg('Mobile Devices', '...\n') + all_mobile = gapi.get_all_pages(cd.mobiledevices(), 'list', 'mobiledevices', page_message=page_message, + customerId=GC_Values[GC_CUSTOMER_ID], query=query, projection=projection, fields=fields, + orderBy=orderBy, sortOrder=sortOrder) + for mobile in all_mobile: + row = {} + for attrib in mobile: + if attrib in ['kind', 'etag']: + continue + if attrib in ['name', 'email', 'otherAccountsInfo']: + if attrib not in titles: + titles.append(attrib) + if listLimit > 0: + row[attrib] = delimiter.join(mobile[attrib][0:listLimit]) + elif listLimit == 0: + row[attrib] = delimiter.join(mobile[attrib]) + elif attrib == 'applications': + if appsLimit >= 0: + if attrib not in titles: + titles.append(attrib) + applications = [] + j = 0 + for app in mobile[attrib]: + j += 1 + if appsLimit and (j > appsLimit): + break + appDetails = [] + for field in ['displayName', 'packageName', 'versionName']: + appDetails.append(app.get(field, '')) + appDetails.append(str(app.get('versionCode', ''))) + permissions = app.get('permission', []) + if permissions: + appDetails.append('/'.join(permissions)) + else: + appDetails.append('') + applications.append('-'.join(appDetails)) + row[attrib] = delimiter.join(applications) + else: + if attrib not in titles: + titles.append(attrib) + if attrib == 'deviceId': + row[attrib] = mobile[attrib].encode('unicode-escape').decode(UTF8) + elif attrib == 'securityPatchLevel' and int(mobile[attrib]): + row[attrib] = utils.formatTimestampYMDHMS(mobile[attrib]) + else: + row[attrib] = mobile[attrib] + csvRows.append(row) + display.sort_csv_titles(['resourceId', 'deviceId', 'serialNumber', 'name', 'email', 'status'], titles) + display.write_csv_file(csvRows, titles, 'Mobile', todrive) + +def doPrintLicenses(returnFields=None, skus=None, countsOnly=False, returnCounts=False): + lic = buildGAPIObject('licensing') + products = [] + licenses = [] + licenseCounts = [] + if not returnFields: + csvRows = [] + todrive = False + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower() + if not returnCounts and myarg == 'todrive': + todrive = True + i += 1 + elif myarg in ['products', 'product']: + products = sys.argv[i+1].split(',') + i += 2 + elif myarg in ['sku', 'skus']: + skus = sys.argv[i+1].split(',') + i += 2 + elif myarg == 'allskus': + skus = sorted(SKUS) + products = [] + i += 1 + elif myarg == 'gsuite': + skus = [skuId for skuId in SKUS if SKUS[skuId]['product'] in ['Google-Apps', '101031']] + products = [] + i += 1 + elif myarg == 'countsonly': + countsOnly = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam print licenses") + if not countsOnly: + fields = 'nextPageToken,items(productId,skuId,userId)' + titles = ['userId', 'productId', 'skuId'] + else: + fields = 'nextPageToken,items(userId)' + if not returnCounts: + if skus: + titles = ['productId', 'skuId', 'licenses'] + else: + titles = ['productId', 'licenses'] + else: + fields = f'nextPageToken,items({returnFields})' + if skus: + for sku in skus: + if not products: + product, sku = getProductAndSKU(sku) + else: + product = products[0] + page_message = gapi.got_total_items_msg(f'Licenses for {SKUS.get(sku, {"displayName": sku})["displayName"]}', '...\n') + try: + licenses += gapi.get_all_pages(lic.licenseAssignments(), 'listForProductAndSku', 'items', throw_reasons=[gapi_errors.ErrorReason.INVALID, gapi_errors.ErrorReason.FORBIDDEN], page_message=page_message, + customerId=GC_Values[GC_DOMAIN], productId=product, skuId=sku, fields=fields) + if countsOnly: + licenseCounts.append(['Product', product, 'SKU', sku, 'Licenses', len(licenses)]) + licenses = [] + except (gapi_errors.GapiInvalidError, gapi_errors.GapiForbiddenError): + pass + else: + if not products: + products = sorted(PRODUCTID_NAME_MAPPINGS) + for productId in products: + page_message = gapi.got_total_items_msg(f'Licenses for {PRODUCTID_NAME_MAPPINGS.get(productId, productId)}', '...\n') + try: + licenses += gapi.get_all_pages(lic.licenseAssignments(), 'listForProduct', 'items', throw_reasons=[gapi_errors.ErrorReason.INVALID, gapi_errors.ErrorReason.FORBIDDEN], page_message=page_message, + customerId=GC_Values[GC_DOMAIN], productId=productId, fields=fields) + if countsOnly: + licenseCounts.append(['Product', productId, 'Licenses', len(licenses)]) + licenses = [] + except (gapi_errors.GapiInvalidError, gapi_errors.GapiForbiddenError): + pass + if countsOnly: + if returnCounts: + return licenseCounts + if skus: + for u_license in licenseCounts: + csvRows.append({'productId': u_license[1], 'skuId': u_license[3], 'licenses': u_license[5]}) + else: + for u_license in licenseCounts: + csvRows.append({'productId': u_license[1], 'licenses': u_license[3]}) + display.write_csv_file(csvRows, titles, 'Licenses', todrive) + return + if returnFields: + if returnFields == 'userId': + userIds = [] + for u_license in licenses: + userId = u_license.get('userId', '').lower() + if userId: + userIds.append(userId) + return userIds + userSkuIds = {} + for u_license in licenses: + userId = u_license.get('userId', '').lower() + skuId = u_license.get('skuId') + if userId and skuId: + userSkuIds.setdefault(userId, []) + userSkuIds[userId].append(skuId) + return userSkuIds + for u_license in licenses: + userId = u_license.get('userId', '').lower() + skuId = u_license.get('skuId', '') + csvRows.append({'userId': userId, 'productId': u_license.get('productId', ''), + 'skuId': _skuIdToDisplayName(skuId)}) + display.write_csv_file(csvRows, titles, 'Licenses', todrive) + +def doShowLicenses(): + licenseCounts = doPrintLicenses(countsOnly=True, returnCounts=True) + for u_license in licenseCounts: + line = '' + for i in range(0, len(u_license), 2): + line += f'{u_license[i]}: {u_license[i+1]}, ' + print(line[:-2]) + +def shlexSplitList(entity, dataDelimiter=' ,'): + lexer = shlex.shlex(entity, posix=True) + lexer.whitespace = dataDelimiter + lexer.whitespace_split = True + return list(lexer) + +def shlexSplitListStatus(entity, dataDelimiter=' ,'): + lexer = shlex.shlex(entity, posix=True) + lexer.whitespace = dataDelimiter + lexer.whitespace_split = True + try: + return (True, list(lexer)) + except ValueError as e: + return (False, str(e)) + +def getQueries(myarg, argstr): + if myarg == 'query': + return [argstr] + return shlexSplitList(argstr) + +def _getRoleVerification(memberRoles, fields): + if memberRoles and memberRoles.find(ROLE_MEMBER) != -1: + return (set(memberRoles.split(',')), None, fields if fields.find('role') != -1 else fields[:-1]+',role)') + return (set(), memberRoles, fields) + +def getUsersToModify(entity_type=None, entity=None, silent=False, member_type=None, checkSuspended=None, groupUserMembersOnly=True): + got_uids = False + if entity_type is None: + entity_type = sys.argv[1].lower() + if entity is None: + entity = sys.argv[2] + cd = buildGAPIObject('directory') + if entity_type == 'user': + users = [entity,] + elif entity_type == 'users': + users = entity.replace(',', ' ').split() + elif entity_type in ['group', 'group_ns', 'group_susp']: + if entity_type == 'group_ns': + checkSuspended = False + elif entity_type == 'group_susp': + checkSuspended = True + got_uids = True + group = entity + if member_type is None: + member_type_message = 'all members' + else: + member_type_message = f'{member_type.lower()}s' + group = normalizeEmailAddressOrUID(group) + page_message = None + if not silent: + sys.stderr.write(f'Getting {member_type_message} of {group} (may take some time for large groups)...\n') + page_message = gapi.got_total_items_msg(f'{member_type_message}', '...') + validRoles, listRoles, listFields = _getRoleVerification(member_type, 'nextPageToken,members(email,id,type,status)') + members = gapi.get_all_pages(cd.members(), 'list', 'members', page_message=page_message, + groupKey=group, roles=listRoles, fields=listFields) + users = [] + for member in members: + if ((not groupUserMembersOnly) or (member['type'] == 'USER')) and _checkMemberRoleIsSuspended(member, validRoles, checkSuspended): + users.append(member.get('email', member['id'])) + elif entity_type in ['ou', 'org', 'ou_ns', 'org_ns', 'ou_susp', 'org_susp',]: + if entity_type in ['ou_ns', 'org_ns']: + checkSuspended = False + elif entity_type in ['ou_susp', 'org_susp']: + checkSuspended = True + got_uids = True + ou = makeOrgUnitPathAbsolute(entity) + users = [] + if ou.startswith('id:'): + ou = gapi.call(cd.orgunits(), 'get', + customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=ou, fields='orgUnitPath')['orgUnitPath'] + query = orgUnitPathQuery(ou, checkSuspended) + page_message = None + if not silent: + printGettingAllItems('Users', query) + page_message = gapi.got_total_items_msg('Users', '...') + members = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + customer=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,users(primaryEmail,orgUnitPath)', + query=query) + ou = ou.lower() + for member in members: + if ou == member.get('orgUnitPath', '').lower(): + users.append(member['primaryEmail']) + if not silent: + sys.stderr.write(f'{len(users)} Users are directly in the OU.\n') + elif entity_type in ['ou_and_children', 'ou_and_child', 'ou_and_children_ns', 'ou_and_child_ns', 'ou_and_children_susp', 'ou_and_child_susp']: + if entity_type in ['ou_and_children_ns', 'ou_and_child_ns']: + checkSuspended = False + elif entity_type in ['ou_and_children_susp', 'ou_and_child_susp']: + checkSuspended = True + got_uids = True + ou = makeOrgUnitPathAbsolute(entity) + users = [] + query = orgUnitPathQuery(ou, checkSuspended) + page_message = None + if not silent: + printGettingAllItems('Users', query) + page_message = gapi.got_total_items_msg('Users', '...') + members = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + customer=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,users(primaryEmail)', + query=query) + for member in members: + users.append(member['primaryEmail']) + if not silent: + sys.stderr.write("done.\r\n") + elif entity_type in ['query', 'queries']: + if entity_type == 'query': + queries = [entity] + else: + queries = shlexSplitList(entity) + got_uids = True + users = [] + usersSet = set() + for query in queries: + if not silent: + printGettingAllItems('Users', query) + page_message = gapi.got_total_items_msg('Users', '...') + members = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + customer=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,users(primaryEmail,suspended)', + query=query) + for member in members: + email = member['primaryEmail'] + if (checkSuspended is None or checkSuspended == member['suspended']) and email not in usersSet: + usersSet.add(email) + users.append(email) + if not silent: + sys.stderr.write("done.\r\n") + elif entity_type in ['license', 'licenses', 'licence', 'licences']: + users = doPrintLicenses(returnFields='userId', skus=entity.split(',')) + elif entity_type in ['file', 'crosfile']: + users = [] + f = fileutils.open_file(entity, strip_utf_bom=True) + for row in f: + user = row.strip() + if user: + users.append(user) + fileutils.close_file(f) + if entity_type == 'crosfile': + entity = 'cros' + elif entity_type in ['csv', 'csvfile', 'croscsv', 'croscsvfile']: + drive, filenameColumn = os.path.splitdrive(entity) + if filenameColumn.find(':') == -1: + controlflow.system_error_exit(2, f'Expected {entity_type} FileName:FieldName') + (filename, column) = filenameColumn.split(':') + f = fileutils.open_file(drive+filename) + input_file = csv.DictReader(f, restval='') + if column not in input_file.fieldnames: + controlflow.csv_field_error_exit(column, input_file.fieldnames) + users = [] + for row in input_file: + user = row[column].strip() + if user: + users.append(user) + fileutils.close_file(f) + if entity_type in ['croscsv', 'croscsvfile']: + entity = 'cros' + elif entity_type in ['courseparticipants', 'teachers', 'students']: + croom = buildGAPIObject('classroom') + users = [] + entity = addCourseIdScope(entity) + if entity_type in ['courseparticipants', 'teachers']: + page_message = gapi.got_total_items_msg('Teachers', '...') + teachers = gapi.get_all_pages(croom.courses().teachers(), 'list', 'teachers', page_message=page_message, courseId=entity) + for teacher in teachers: + email = teacher['profile'].get('emailAddress', None) + if email: + users.append(email) + if entity_type in ['courseparticipants', 'students']: + page_message = gapi.got_total_items_msg('Students', '...') + students = gapi.get_all_pages(croom.courses().students(), 'list', 'students', page_message=page_message, courseId=entity) + for student in students: + email = student['profile'].get('emailAddress', None) + if email: + users.append(email) + elif entity_type == 'all': + got_uids = True + users = [] + entity = entity.lower() + if entity == 'users': + query = 'isSuspended=False' + if not silent: + printGettingAllItems('Users', None) + page_message = gapi.got_total_items_msg('Users', '...') + all_users = gapi.get_all_pages(cd.users(), 'list', 'users', page_message=page_message, + customer=GC_Values[GC_CUSTOMER_ID], query=query, + fields='nextPageToken,users(primaryEmail)') + for member in all_users: + users.append(member['primaryEmail']) + if not silent: + sys.stderr.write(f"done getting {len(users)} Users.\r\n") + elif entity == 'cros': + if not silent: + printGettingAllItems('CrOS Devices', None) + page_message = gapi.got_total_items_msg('CrOS Devices', '...') + all_cros = gapi.get_all_pages(cd.chromeosdevices(), 'list', 'chromeosdevices', page_message=page_message, + customerId=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,chromeosdevices(deviceId)') + for member in all_cros: + users.append(member['deviceId']) + if not silent: + sys.stderr.write(f"done getting {len(users)} CrOS Devices.\r\n") + else: + controlflow.invalid_argument_exit(entity, "gam all") + elif entity_type == 'cros': + users = entity.replace(',', ' ').split() + entity = 'cros' + elif entity_type in ['crosquery', 'crosqueries', 'cros_sn']: + if entity_type == 'cros_sn': + queries = [f'id:{sn}' for sn in shlexSplitList(entity)] + elif entity_type == 'crosqueries': + queries = shlexSplitList(entity) + else: + queries = [entity] + users = [] + usersSet = set() + for query in queries: + if not silent: + printGettingAllItems('CrOS Devices', query) + page_message = gapi.got_total_items_msg('CrOS Devices', '...') + members = gapi.get_all_pages(cd.chromeosdevices(), 'list', 'chromeosdevices', page_message=page_message, + customerId=GC_Values[GC_CUSTOMER_ID], fields='nextPageToken,chromeosdevices(deviceId)', + query=query) + for member in members: + deviceId = member['deviceId'] + if deviceId not in usersSet: + usersSet.add(deviceId) + users.append(deviceId) + if not silent: + sys.stderr.write("done.\r\n") + entity = 'cros' + else: + controlflow.invalid_argument_exit(entity_type, "gam") + full_users = list() + if entity != 'cros' and not got_uids: + for user in users: + cg = UID_PATTERN.match(user) + if cg: + full_users.append(cg.group(1)) + elif user != '*' and user != GC_Values[GC_CUSTOMER_ID] and user.find('@') == -1: + full_users.append(f'{user}@{GC_Values[GC_DOMAIN]}') + else: + full_users.append(user) + else: + full_users = users + return full_users + +def OAuthInfo(): + credentials = access_token = id_token = None + show_secret = False + i = 3 + while i < len(sys.argv): + myarg = sys.argv[i].lower().replace('_', '') + if myarg == 'accesstoken': + access_token = sys.argv[i+1] + i += 2 + elif myarg == 'idtoken': + id_token = sys.argv[i+1] + i += 2 + elif myarg == 'showsecret': + show_secret = True + i += 1 + else: + controlflow.invalid_argument_exit(sys.argv[i], "gam oauth info") + if not access_token and not id_token: + credentials = getValidOauth2TxtCredentials() + access_token = credentials.token + print(f'\nOAuth File: {GC_Values[GC_OAUTH2_TXT]}') + oa2 = buildGAPIObject('oauth2') + token_info = gapi.call(oa2, 'tokeninfo', access_token=access_token, id_token=id_token) + if 'issued_to' in token_info: + print(f'Client ID: {token_info["issued_to"]}') + if credentials is not None and show_secret: + print(f'Secret: {credentials.client_secret}') + if 'scope' in token_info: + scopes = token_info['scope'].split(' ') + print(f'Scopes ({len(scopes)})') + for scope in sorted(scopes): + print(f' {scope}') + if 'email' in token_info: + print(f'G Suite Admin: {token_info["email"]}') + if 'expires_in' in token_info: + expires = (datetime.datetime.now() + datetime.timedelta(seconds=token_info['expires_in'])).isoformat() + print(f'Expires: {expires}') + for key, value in token_info.items(): + if key not in ['issued_to', 'scope', 'email', 'expires_in']: + print(f'{key}: {value}') + +def doDeleteOAuth(): + credentials = getOauth2TxtStorageCredentials() + if credentials is None: + return + sys.stderr.write('This OAuth token will self-destruct in 3...') + sys.stderr.flush() + time.sleep(1) + sys.stderr.write('2...') + sys.stderr.flush() + time.sleep(1) + sys.stderr.write('1...') + sys.stderr.flush() + time.sleep(1) + sys.stderr.write('boom!\n') + sys.stderr.flush() + credentials.revoke() + credentials.delete() + +def doRequestOAuth(login_hint=None): + missing_client_secrets_message = ('To use GAM you need to create an API ' + 'project. Please run:\n\ngam create project') + client_secrets_file = GC_Values[GC_CLIENT_SECRETS_JSON] + invalid_client_secrets_format_message = ('The format of your client secrets ' + 'file:\n\n%s\n\nis incorrect. ' + 'Please recreate the file.' % + client_secrets_file) + stored_creds = getOauth2TxtStorageCredentials() + if stored_creds and stored_creds.valid: + print('It looks like you\'ve already authorized GAM. Refusing to overwrite existing file:\n\n%s' % stored_creds.filename) + return + + scopes = getScopesFromUser() + if scopes is None: + # There were no scopes selected. Exit cleanly. + controlflow.system_error_exit(0, '') + login_hint = _getValidateLoginHint(login_hint) + # Needs to be set so oauthlib doesn't puke when Google changes our scopes + os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = 'true' + try: + creds = gam.auth.oauth.Credentials.from_client_secrets_file( + client_secrets_file=client_secrets_file, + scopes=scopes, + access_type='offline', + login_hint=login_hint, + credentials_file=GC_Values[GC_OAUTH2_TXT], + use_console_flow=not GC_Values[GC_OAUTH_BROWSER]) + creds.write() + except gam.auth.oauth.InvalidClientSecretsFileError: + controlflow.system_error_exit(14, missing_client_secrets_message) + except gam.auth.oauth.InvalidClientSecretsFileFormatError: + controlflow.system_error_exit(3, invalid_client_secrets_format_message) + +OAUTH2_SCOPES = [ + {'name': 'Classroom API - counts as 5 scopes', + 'subscopes': [], + 'scopes': ['https://www.googleapis.com/auth/classroom.rosters', + 'https://www.googleapis.com/auth/classroom.courses', + 'https://www.googleapis.com/auth/classroom.profile.emails', + 'https://www.googleapis.com/auth/classroom.profile.photos', + 'https://www.googleapis.com/auth/classroom.guardianlinks.students']}, + {'name': 'Cloud Print API', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/cloudprint'}, + {'name': 'Data Transfer API', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.datatransfer'}, + {'name': 'Directory API - Chrome OS Devices', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.device.chromeos'}, + {'name': 'Directory API - Customers', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.customer'}, + {'name': 'Directory API - Domains', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.domain'}, + {'name': 'Directory API - Groups', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.group'}, + {'name': 'Directory API - Mobile Devices', + 'subscopes': ['readonly', 'action'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.device.mobile'}, + {'name': 'Directory API - Organizational Units', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.orgunit'}, + {'name': 'Directory API - Resource Calendars', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.resource.calendar'}, + {'name': 'Directory API - Roles', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.rolemanagement'}, + {'name': 'Directory API - User Schemas', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.userschema'}, + {'name': 'Directory API - User Security', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.user.security'}, + {'name': 'Directory API - Users', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/admin.directory.user'}, + {'name': 'Group Settings API', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/apps.groups.settings'}, + {'name': 'License Manager API', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/apps.licensing'}, + {'name': 'Pub / Sub API', + 'subscopes': [], + 'offByDefault': True, + 'scopes': 'https://www.googleapis.com/auth/pubsub'}, + {'name': 'Reports API - Audit Reports', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/admin.reports.audit.readonly'}, + {'name': 'Reports API - Usage Reports', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/admin.reports.usage.readonly'}, + {'name': 'Reseller API', + 'subscopes': [], + 'offByDefault': True, + 'scopes': 'https://www.googleapis.com/auth/apps.order'}, + {'name': 'Site Verification API', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/siteverification'}, + {'name': 'Vault Matters and Holds API', + 'subscopes': ['readonly'], + 'scopes': 'https://www.googleapis.com/auth/ediscovery'}, + {'name': 'Cloud Storage (Vault Export - read only)', + 'subscopes': [], + 'scopes': 'https://www.googleapis.com/auth/devstorage.read_only'}, + {'name': 'User Profile (Email address - read only)', + 'subscopes': [], + 'scopes': 'email', + 'required': True}, + ] + +def getScopesFromUser(menu_options=None): + """Prompts the user to choose from a list of scopes to authorize. + + Args: + menu_options: An optional list of ScopeMenuOptions to be presented in the + menu. If no menu_options are provided, menu options will be generated + from static OAUTH2_SCOPES definitions. + + Returns: + A list of user-selected scopes to authorize. + """ + + if not menu_options: + menu_options = [ScopeMenuOption.from_gam_oauth2scope(definition) + for definition in OAUTH2_SCOPES] + menu = ScopeSelectionMenu(menu_options) + try: + menu.run() + except ScopeSelectionMenu.UserRequestedExitException: + controlflow.system_error_exit(0, '') + + return menu.get_selected_scopes() + +class ScopeMenuOption(): + """A single GAM API/feature with scopes that can be turned on/off.""" + + def __init__(self, oauth_scopes, description, + is_required=False, + is_selected=False, + supported_restrictions=None, + restriction=None): + """A data structure for storing and toggling feature/API scope attributes. + + Args: + oauth_scopes: A list of Google OAuth scope strings required for the + feature or API. If the applicable scopes can vary in permission level, + the scopes provided in this list should contain the highest level of + permissions. More restrictive scopes are implemented by utilizing the + `supported_restrictions` argument. + description: String, a name or brief description of this API/feature. + is_required: Bool, whether this API/feature is required for GAM. If True, + the ScopeMenuOption cannot be unselected/disabled. + is_selected: Bool, whether the ScopeMenuOption is currently + selected/enabled. + supported_restrictions: A list of strings that can be appended to the + oauth_scopes, separated by '.', to restrict their permissions. + For example, the directory API supports a 'readonly' mode on most + scopes such that + https://www.googleapis.com/auth/admin.directory.domain + becomes + https://www.googleapis.com/auth/admin.directory.domain.readonly + restriction: String, the currently enabled restriction on all scopes in + this ScopeMenuOption. Default is no restrictions (highest permission). + """ + # Initialize private members + self._is_selected = False + self._restriction = None + + self.scopes = oauth_scopes + self.description = description + self.is_required = is_required + # Required scopes must be selected + self.is_selected = is_required or is_selected + self.supported_restrictions = ( + supported_restrictions if supported_restrictions is not None else []) + if restriction: + self.restrict_to(restriction) + + def select(self, restriction=None): + """Selects/enables the ScopeMenuOption with an optional restriction.""" + if restriction is not None: + self.restrict_to(restriction) + self.is_selected = True + + def unselect(self): + """Unselects/disables the ScopeMenuOption.""" + self.is_selected = False + + @property + def is_selected(self): + return self._is_selected + + @is_selected.setter + def is_selected(self, is_selected): + if self.is_required and not is_selected: + raise ValueError('Required scope cannot be unselected') + if not is_selected: + # Disable all applied restrictions + self.unrestrict() + self._is_selected = is_selected + + @property + def is_restricted(self): + return self._restriction is not None + + def supports_restriction(self, restriction): + """Determines if a scope restriction is supported by this ScopeMenuOption. + + Args: + restriction: String, the text appended to a full permission scope which + will restrict its permissiveness. e.g. 'readonly' or 'action'. + + Returns: + Bool, True if the scope restriction can be applied to this option. + """ + return restriction in self.supported_restrictions + + @property + def restriction(self): + return self._restriction + + @restriction.setter + def restriction(self, restriction): + self.restrict_to(restriction) + + def restrict_to(self, restriction): + """Applies a scope restriction to all scopes associated with this option. + + Args: + restriction: String, a scope restriction which is appended to the + full-permission scope with a leading '.'. e.g. if the full scope is + https://www.googleapis.com/auth/admin.directory.domain + providing 'readonly' here will make the effective scope + https://www.googleapis.com/auth/admin.directory.domain.readonly + """ + if self.supports_restriction(restriction): + self._restriction = restriction + else: + error = f'Scope does not support a {restriction} restriction.' + if self.supported_restrictions is not None: + restriction_list = ', '.join(self.supported_restrictions) + error = error + (f' Supported restrictions are: {restriction_list}') + raise ValueError(error) + + def unrestrict(self): + """Removes all scope restrictions currently applied.""" + self._restriction = None + + def get_effective_scopes(self): + """Gets all scopes for this option, including their restrictions. + + Restrictions are applied in the form of trailing text which limit the + scope's capabilities. + """ + effective_scopes = [] + for scope in self.scopes: + if self.is_restricted: + scope = f'{scope}.{self._restriction}' + effective_scopes.append(scope) + return effective_scopes + + @classmethod + def from_gam_oauth2scope(cls, scope_definition): + """Generates a ScopeMenuOption from a dict-style OAUTH2_SCOPES definition. + + Dict fields: + name: Some description of the API/feature. + subscopes: A list of compatible scope restrictions such as 'action' or + 'readonly'. Each scope in the scopes list must support this + restriction text appended to the end of its normal scope text. + scopes: A list of scopes that are required for the API/feature. + offByDefault: A bool indicating whether this feature/scope should be off + by default (when no prior selection has been made). Default is False + (the item will be on/selected by default). + required: A bool indicating the API/feature is required. This scope + cannot be unselected. Default is False. + + Example: + { + 'name': 'Made up API', + 'subscopes': ['action'], + 'scopes': ['https://www.googleapis.com/auth/some.scope'], + } + + Args: + scope_definition: A dict following the syntax of scopes defined in + OAUTH2_SCOPES. + + Returns: + A ScopeMenuOption object. + """ + scopes = scope_definition.get('scopes', []) + # If the scope is a single string, make it into a list. + scope_list = scopes if isinstance(scopes, list) else [scopes] + return cls( + oauth_scopes=scope_list, + description=scope_definition.get('name'), + is_selected=not scope_definition.get('offByDefault'), + supported_restrictions=scope_definition.get('subscopes', []), + is_required=scope_definition.get('required', False)) + +class ScopeSelectionMenu(): + """A text menu which prompts the user to select the scopes to authorize.""" + + class MenuChoiceError(Exception): + """Error when an invalid or incompatible user choice is made.""" + pass + + class UserRequestedExitException(Exception): + """Exception when the user requests immediate exit.""" + pass + + def __init__(self, options): + """A menu of scope options to choose from. + + Args: + options: A list of ScopeMenuOption objects from which to generate the menu + Options will be presented on screen in the same order as the provided + list. + """ + self._options = options + + def get_options(self): + """Returns all options that are available on this menu.""" + return self._options + + def get_selected_options(self): + """Returns all currently selected ScopeMenuOptions.""" + return [option for option in self._options if option.is_selected] + + def get_selected_scopes(self): + """Returns the aggregate set of oauth scopes currently selected.""" + selected_scopes = [scope for option in self.get_selected_options() + for scope in option.get_effective_scopes()] + return list(set(selected_scopes)) + + MENU_CHOICE = { + 'SELECT_ALL_SCOPES': 's', + 'UNSELECT_ALL_SCOPES': 'u', + 'EXIT': 'e', + 'CONTINUE': 'c' + } + + _MENU_DISPLAY_TEXT = ''' +Select the authorized scopes by entering a number. +Append an 'r' to grant read-only access or an 'a' to grant action-only access. + +%s + + s) Select all scopes + u) Unselect all scopes + e) Exit without changes + c) Continue to authorization + +''' + + def get_menu_text(self): + """Returns a text menu with numbered options.""" + scope_menu_items = [ + self._build_scope_menu_item(option, counter) + for counter, option in enumerate(self._options) + ] + return ScopeSelectionMenu._MENU_DISPLAY_TEXT % '\n'.join(scope_menu_items) + + @staticmethod + def _build_scope_menu_item(scope_option, option_number): + """Builds a text line representing a single scope selection in the menu. + + The returned line is in the format: + + [<*>] <##>) () [required] + + * = A single character indicating the option's selection status. + '*' - All scopes are selected with full permissions. + ' ' - No scopes are selected. + 'X' - Where 'X' is the first letter of the selected scope restriction, + such as 'R' for readonly or 'A' for action, indicates scopes are + selected with the corresponding restriction. + ## = The line item number associated to this option. + Feature description = The ScopeMenuOption description. + scope modifiers = The supported scope restrictions for the ScopeMenuOption. + When appended to the unrestricted, full oauth scope, these strings + modify or restrict the access level of the given scope. + [required] = Will only appear if the ScopeMenuOption is required and cannot + be unselected. + + e.g. [*] 2) Directory API - Domains (supports 'readonly') + + Args: + scope_option: The ScopeMenuOption associated with this line item. + option_number: The selectable option number that is associated with + modifying this line item. + + Returns: + A string containing the line item text without a trailing newline. + """ + SELECTION_INDICATOR = { + 'ALL_SELECTED': '*', + 'UNSELECTED': ' ', + } + indicator = SELECTION_INDICATOR['UNSELECTED'] + if scope_option.is_selected: + if scope_option.is_restricted: + # Use the first letter of the restriction as the indicator. + indicator = scope_option.restriction[0].upper() + else: + indicator = SELECTION_INDICATOR['ALL_SELECTED'] + + item_description = [f'[{indicator}]', f'{option_number:2d})', scope_option.description,] + + if scope_option.supported_restrictions: + restrictions = ' and '.join(scope_option.supported_restrictions) + item_description.append(f'(supports {restrictions})') + + if scope_option.is_required: + item_description.append('[required]') + + return ' '.join(item_description) + + def get_prompt_text(self): + """Builds and returns a prompt requesting user input.""" + # Get all the available restrictions and create the list of available + # commands that the user can input. + restrictions = { + restriction for option in self._options + for restriction in option.supported_restrictions + } + restriction_choices = [ + restriction[0].lower() for restriction in restrictions + ] + return ('Please enter 0-%d[%s] or %s: ' % + (len(self._options)-1, # Keep the menu options 0-based + '|'.join(restriction_choices), + '|'.join(list(ScopeSelectionMenu.MENU_CHOICE.values())))) + + def run(self): + """Displays the ScopeSelectionMenu to the user and prompts for input. + + The menu will continue to display until the user finishes adjusting all + desired items and requests to return. + + After the menu is run, callers may use `get_selected_scopes()` or + `get_selected_options()` methods to understand what the user's final choice + was. + + Raises: ScopeSelectionMenu.UserRequestedExitException if the user chooses + to exit the application entirely, rather than continue execution. This + allows callers to decide how to handle the exit. + """ + error_message = None + while True: + os.system(['clear', 'cls'][GM_Globals[GM_WINDOWS]]) + sys.stdout.write(self.get_menu_text()) + if error_message is not None: + colored_error = createRedText(ERROR_PREFIX + error_message + '\n') + sys.stdout.write(colored_error) + error_message = None # Clear the pending error message + + user_input = input(self.get_prompt_text()) + try: + prompt_again = self._process_menu_input(user_input) + if not prompt_again: + return + except ScopeSelectionMenu.MenuChoiceError as e: + error_message = str(e) + + _SINGLE_SCOPE_CHANGE_REGEX = re.compile( + r'\s*(?P\d{1,2})\s*(?P[a-z]?)', re.IGNORECASE) + + # Google-defined maximum number of scopes that can be authorized on a single + # access token. + MAXIMUM_NUM_SCOPES = 50 + + def _process_menu_input(self, raw_menu_input): + """Processes the raw user input provided to the menu prompt. + + Args: + raw_menu_input: The raw, unaltered string provided by the user in response + to the menu prompt. + + Returns: + True, if the user should be prompted for further input. False, if the + user has finished input and requested to continue execution of the + program. + + Raises: + ScopeSelectionMenu.UserRequestedExitException if the user requests to exit + the application immediately. + ScopeSelectionMenu.MenuChoiceError upon invalid user input. + """ + user_input = raw_menu_input.lower().strip() + single_scope_change = ( + ScopeSelectionMenu._SINGLE_SCOPE_CHANGE_REGEX.match(user_input)) + + if single_scope_change: + scope_number, restriction_command = single_scope_change.group( + 'scope_number', 'restriction') + # Make sure we get an actual number to deal with. + scope_number = int(scope_number) + # Scope option numbers displayed in the menu are 0-based and map directly + # to the indices in the list of scopes. + if scope_number < 0 or scope_number > len(self._options) - 1: + raise ScopeSelectionMenu.MenuChoiceError( + f'Invalid scope number "{scope_number}"') + selected_option = self._options[scope_number] + + # Find the restriction that the user intended to apply. + if restriction_command != '': + matching_restrictions = [r for r in selected_option.supported_restrictions if r.startswith(restriction_command)] + if not matching_restrictions: + raise ScopeSelectionMenu.MenuChoiceError( + f'Scope "{selected_option.description}" does not support "{restriction_command}" mode!') + restriction = matching_restrictions[0] + else: + restriction = None + self._update_option(selected_option, restriction=restriction) + + elif user_input == ScopeSelectionMenu.MENU_CHOICE['SELECT_ALL_SCOPES']: + for option in self._options: + self._update_option(option, selected=True) + elif user_input == ScopeSelectionMenu.MENU_CHOICE['UNSELECT_ALL_SCOPES']: + for option in self._options: + # Force-select required options + self._update_option(option, selected=option.is_required) + elif user_input == ScopeSelectionMenu.MENU_CHOICE['CONTINUE']: + return False + elif user_input == ScopeSelectionMenu.MENU_CHOICE['EXIT']: + raise ScopeSelectionMenu.UserRequestedExitException() + else: + raise ScopeSelectionMenu.MenuChoiceError( + f'Invalid input "{user_input}"') + + return True + + def _update_option(self, option, selected=None, restriction=None): + """Validates changes and updates the internal state of options on the menu. + + Args: + option: The ScopeMenuOption to update + selected: If provided, updates the "selected" status of the option. If + not provided, the "selected" status will be toggled to its opposite + state. + restriction: If provided, applies a restriction to the provided option. + + Raises: + ScopeSelectionMenu.MenuChoiceError on change validation errors. + """ + if option.is_required and (not selected or selected is None): + raise ScopeSelectionMenu.MenuChoiceError( + f'Scope "{option.description}" is required and cannot be unselected!') + if selected and not option.is_selected: + # Make sure we're not about to exceed the maximum number of scopes + # authorized on a single token. + num_scopes_to_add = len(option.get_effective_scopes()) + num_selected_scopes = len(self.get_selected_scopes()) + expected_num_scopes = num_scopes_to_add + num_selected_scopes + if expected_num_scopes > ScopeSelectionMenu.MAXIMUM_NUM_SCOPES: + raise ScopeSelectionMenu.MenuChoiceError( + f'Too many scopes selected ({expected_num_scopes}). Maximum is ' + f'{ScopeSelectionMenu.MAXIMUM_NUM_SCOPES}.Please remove some scopes ' + 'and try again.') + + if restriction is None: + if selected is None: + # Toggle the option on/off + option.is_selected = not option.is_selected + else: + option.is_selected = selected + else: + if option.supports_restriction(restriction): + option.select(restriction) + else: + raise ScopeSelectionMenu.MenuChoiceError( + f'Scope "{option.description}" does not support {restriction} mode!') + +def init_gam_worker(): + signal.signal(signal.SIGINT, signal.SIG_IGN) + +def run_batch(items): + if not items: + return + num_worker_threads = min(len(items), GC_Values[GC_NUM_THREADS]) + pool = mp_pool(num_worker_threads, init_gam_worker) + sys.stderr.write(f'Using {num_worker_threads} processes...\n') + try: + results = [] + for item in items: + if item[0] == 'commit-batch': + sys.stderr.write('commit-batch - waiting for running processes to finish before proceeding\n') + pool.close() + pool.join() + pool = mp_pool(num_worker_threads, init_gam_worker) + sys.stderr.write('commit-batch - running processes finished, proceeding\n') + continue + results.append(pool.apply_async(ProcessGAMCommandMulti, [item])) + pool.close() + num_total = len(results) + i = 1 + while True: + num_done = 0 + for r in results: + if r.ready(): + num_done += 1 + if num_done == num_total: + break + i += 1 + if i == 20: + print(f'Finished {num_done} of {num_total} processes.') + i = 1 + time.sleep(1) + except KeyboardInterrupt: + pool.terminate() + pool.join() + +# +# Process command line arguments, find substitutions +# An argument containing instances of ~~xxx~~ has xxx replaced by the value of field xxx from the CSV file +# An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file +# Otherwise, the argument is preserved as is +# +# SubFields is a dictionary; the key is the argument number, the value is a list of tuples that mark +# the substition (fieldname, start, end). +# Example: update user '~User' address type work unstructured '~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~' primary +# {2: [('User', 0, 5)], 7: [('Street', 0, 10), ('City', 12, 20), ('State', 22, 31), ('ZIP', 32, 39)]} +# +def getSubFields(i, fieldNames): + subFields = {} + PATTERN = re.compile(r'~~(.+?)~~') + GAM_argv = [] + GAM_argvI = 0 + while i < len(sys.argv): + myarg = sys.argv[i] + if not myarg: + GAM_argv.append(myarg) + elif PATTERN.search(myarg): + pos = 0 + while True: + match = PATTERN.search(myarg, pos) + if not match: + break + fieldName = match.group(1) + if fieldName in fieldNames: + subFields.setdefault(GAM_argvI, []) + subFields[GAM_argvI].append((fieldName, match.start(), match.end())) + else: + controlflow.csv_field_error_exit(fieldName, fieldNames) + pos = match.end() + GAM_argv.append(myarg) + elif myarg[0] == '~': + fieldName = myarg[1:] + if fieldName in fieldNames: + subFields[GAM_argvI] = [(fieldName, 0, len(myarg))] + GAM_argv.append(myarg) + else: + controlflow.csv_field_error_exit(fieldName, fieldNames) + else: + GAM_argv.append(myarg) + GAM_argvI += 1 + i += 1 + return(GAM_argv, subFields) +# +def processSubFields(GAM_argv, row, subFields): + argv = GAM_argv[:] + for GAM_argvI, fields in subFields.items(): + oargv = argv[GAM_argvI][:] + argv[GAM_argvI] = '' + pos = 0 + for field in fields: + argv[GAM_argvI] += oargv[pos:field[1]] + if row[field[0]]: + argv[GAM_argvI] += row[field[0]] + pos = field[2] + argv[GAM_argvI] += oargv[pos:] + return argv + +def runCmdForUsers(cmd, users, default_to_batch=False, **kwargs): + if default_to_batch and len(users) > 1: + items = [] + for user in users: + items.append(['gam', 'user', user] + sys.argv[3:]) + run_batch(items) + sys.exit(0) + else: + cmd(users, **kwargs) + +def ProcessGAMCommandMulti(args): + ProcessGAMCommand(args) + +# Process GAM command +def ProcessGAMCommand(args): + if args != sys.argv: + sys.argv = args[:] + GM_Globals[GM_SYSEXITRC] = 0 + try: + SetGlobalVariables() + if sys.version_info[1] >= 7: + sys.stdout.reconfigure(encoding=GC_Values[GC_CHARSET], errors='backslashreplace') + sys.stdin.reconfigure(encoding=GC_Values[GC_CHARSET], errors='backslashreplace') + command = sys.argv[1].lower() + if command == 'batch': + i = 2 + filename = sys.argv[i] + i, encoding = getCharSet(i+1) + f = fileutils.open_file(filename, encoding=encoding, strip_utf_bom=True) + items = [] + errors = 0 + for line in f: + try: + argv = shlex.split(line) + except ValueError as e: + sys.stderr.write(f'Command: >>>{line.strip()}<<<\n') + sys.stderr.write(f'{ERROR_PREFIX}{str(e)}\n') + errors += 1 + continue + if argv: + cmd = argv[0].strip().lower() + if (not cmd) or cmd.startswith('#') or ((len(argv) == 1) and (cmd != 'commit-batch')): + continue + if cmd == 'gam': + items.append(argv) + elif cmd == 'commit-batch': + items.append([cmd]) + else: + sys.stderr.write(f'Command: >>>{line.strip()}<<<\n') + sys.stderr.write(f'{ERROR_PREFIX}Invalid: Expected \n') + errors += 1 + fileutils.close_file(f) + if errors == 0: + run_batch(items) + sys.exit(0) + else: + controlflow.system_error_exit(2, f'batch file: {filename}, not processed, {errors} error{["", "s"][errors != 1]}') + elif command == 'csv': + if httplib2.debuglevel > 0: + controlflow.system_error_exit(1, 'CSV commands are not compatible with debug. Delete debug.gam and try again.') + i = 2 + filename = sys.argv[i] + i, encoding = getCharSet(i+1) + f = fileutils.open_file(filename, encoding=encoding) + csvFile = csv.DictReader(f) + if (i == len(sys.argv)) or (sys.argv[i].lower() != 'gam') or (i+1 == len(sys.argv)): + controlflow.system_error_exit(3, '"gam csv " must be followed by a full GAM command...') + i += 1 + GAM_argv, subFields = getSubFields(i, csvFile.fieldnames) + items = [] + for row in csvFile: + items.append(['gam']+processSubFields(GAM_argv, row, subFields)) + fileutils.close_file(f) + run_batch(items) + sys.exit(0) + elif command == 'version': + doGAMVersion() + sys.exit(0) + elif command == 'create': + argument = sys.argv[2].lower() + if argument == 'user': + doCreateUser() + elif argument == 'group': + doCreateGroup() + elif argument in ['nickname', 'alias']: + doCreateAlias() + elif argument in ['org', 'ou']: + doCreateOrg() + elif argument == 'resource': + gapi_directory_resource.createResourceCalendar() + elif argument in ['verify', 'verification']: + doSiteVerifyShow() + elif argument == 'schema': + doCreateOrUpdateUserSchema(False) + elif argument in ['course', 'class']: + doCreateCourse() + elif argument in ['transfer', 'datatransfer']: + doCreateDataTransfer() + elif argument == 'domain': + doCreateDomain() + elif argument in ['domainalias', 'aliasdomain']: + doCreateDomainAlias() + elif argument == 'admin': + doCreateAdmin() + elif argument in ['guardianinvite', 'inviteguardian', 'guardian']: + doInviteGuardian() + elif argument in ['project', 'apiproject']: + doCreateProject() + elif argument in ['resoldcustomer', 'resellercustomer']: + doCreateResoldCustomer() + elif argument in ['resoldsubscription', 'resellersubscription']: + doCreateResoldSubscription() + elif argument in ['matter', 'vaultmatter']: + gapi_vault.createMatter() + elif argument in ['hold', 'vaulthold']: + gapi_vault.createHold() + elif argument in ['export', 'vaultexport']: + gapi_vault.createExport() + elif argument in ['building']: + gapi_directory_resource.createBuilding() + elif argument in ['feature']: + gapi_directory_resource.createFeature() + elif argument in ['alertfeedback']: + doCreateAlertFeedback() + elif argument in ['gcpfolder']: + createGCPFolder() + else: + controlflow.invalid_argument_exit(argument, "gam create") + sys.exit(0) + elif command == 'use': + argument = sys.argv[2].lower() + if argument in ['project', 'apiproject']: + doUseProject() + else: + controlflow.invalid_argument_exit(argument, "gam use") + sys.exit(0) + elif command == 'update': + argument = sys.argv[2].lower() + if argument == 'user': + doUpdateUser(None, 4) + elif argument == 'group': + doUpdateGroup() + elif argument in ['nickname', 'alias']: + doUpdateAlias() + elif argument in ['ou', 'org']: + doUpdateOrg() + elif argument == 'resource': + gapi_directory_resource.updateResourceCalendar() + elif argument == 'cros': + gapi_directory_cros.doUpdateCros() + elif argument == 'mobile': + doUpdateMobile() + elif argument in ['verify', 'verification']: + doSiteVerifyAttempt() + elif argument in ['schema', 'schemas']: + doCreateOrUpdateUserSchema(True) + elif argument in ['course', 'class']: + doUpdateCourse() + elif argument in ['printer', 'print']: + doUpdatePrinter() + elif argument == 'domain': + doUpdateDomain() + elif argument == 'customer': + gapi_directory_customer.doUpdateCustomer() + elif argument in ['resoldcustomer', 'resellercustomer']: + doUpdateResoldCustomer() + elif argument in ['resoldsubscription', 'resellersubscription']: + doUpdateResoldSubscription() + elif argument in ['matter', 'vaultmatter']: + gapi_vault.updateMatter() + elif argument in ['hold', 'vaulthold']: + gapi_vault.updateHold() + elif argument in ['project', 'projects', 'apiproject']: + doUpdateProjects() + elif argument in ['building']: + gapi_directory_resource.updateBuilding() + elif argument in ['feature']: + gapi_directory_resource.updateFeature() + else: + controlflow.invalid_argument_exit(argument, "gam update") + sys.exit(0) + elif command == 'info': + argument = sys.argv[2].lower() + if argument == 'user': + doGetUserInfo() + elif argument == 'group': + doGetGroupInfo() + elif argument == 'member': + doGetMemberInfo() + elif argument in ['nickname', 'alias']: + doGetAliasInfo() + elif argument == 'instance': + gapi_directory_customer.doGetCustomerInfo() + elif argument in ['org', 'ou']: + doGetOrgInfo() + elif argument == 'resource': + gapi_directory_resource.getResourceCalendarInfo() + elif argument == 'cros': + gapi_directory_cros.doGetCrosInfo() + elif argument == 'mobile': + doGetMobileInfo() + elif argument in ['verify', 'verification']: + doGetSiteVerifications() + elif argument in ['schema', 'schemas']: + doGetUserSchema() + elif argument in ['course', 'class']: + doGetCourseInfo() + elif argument in ['printer', 'print']: + doGetPrinterInfo() + elif argument in ['transfer', 'datatransfer']: + doGetDataTransferInfo() + elif argument == 'customer': + gapi_directory_customer.doGetCustomerInfo() + elif argument == 'domain': + doGetDomainInfo() + elif argument in ['domainalias', 'aliasdomain']: + doGetDomainAliasInfo() + elif argument in ['resoldcustomer', 'resellercustomer']: + doGetResoldCustomer() + elif argument in ['resoldsubscription', 'resoldsubscriptions', 'resellersubscription', 'resellersubscriptions']: + doGetResoldSubscriptions() + elif argument in ['matter', 'vaultmatter']: + gapi_vault.getMatterInfo() + elif argument in ['hold', 'vaulthold']: + gapi_vault.getHoldInfo() + elif argument in ['export', 'vaultexport']: + gapi_vault.getExportInfo() + elif argument in ['building']: + gapi_directory_resource.getBuildingInfo() + else: + controlflow.invalid_argument_exit(argument, "gam info") + sys.exit(0) + elif command == 'cancel': + argument = sys.argv[2].lower() + if argument in ['guardianinvitation', 'guardianinvitations']: + doCancelGuardianInvitation() + else: + controlflow.invalid_argument_exit(argument, "gam cancel") + sys.exit(0) + elif command == 'delete': + argument = sys.argv[2].lower() + if argument == 'user': + doDeleteUser() + elif argument == 'group': + doDeleteGroup() + elif argument in ['nickname', 'alias']: + doDeleteAlias() + elif argument == 'org': + doDeleteOrg() + elif argument == 'resource': + gapi_directory_resource.deleteResourceCalendar() + elif argument == 'mobile': + doDeleteMobile() + elif argument in ['schema', 'schemas']: + doDelSchema() + elif argument in ['course', 'class']: + doDelCourse() + elif argument in ['printer', 'printers']: + doDelPrinter() + elif argument == 'domain': + doDelDomain() + elif argument in ['domainalias', 'aliasdomain']: + doDelDomainAlias() + elif argument == 'admin': + doDelAdmin() + elif argument in ['guardian', 'guardians']: + doDeleteGuardian() + elif argument in ['project', 'projects']: + doDelProjects() + elif argument in ['resoldsubscription', 'resellersubscription']: + doDeleteResoldSubscription() + elif argument in ['matter', 'vaultmatter']: + gapi_vault.updateMatter(action=command) + elif argument in ['hold', 'vaulthold']: + gapi_vault.deleteHold() + elif argument in ['export', 'vaultexport']: + gapi_vault.deleteExport() + elif argument in ['building']: + gapi_directory_resource.deleteBuilding() + elif argument in ['feature']: + gapi_directory_resource.deleteFeature() + elif argument in ['alert']: + doDeleteOrUndeleteAlert('delete') + elif argument in ['sakey', 'sakeys']: + doDeleteServiceAccountKeys() + else: + controlflow.invalid_argument_exit(argument, "gam delete") + sys.exit(0) + elif command == 'undelete': + argument = sys.argv[2].lower() + if argument == 'user': + doUndeleteUser() + elif argument in ['matter', 'vaultmatter']: + gapi_vault.updateMatter(action=command) + elif argument == 'alert': + doDeleteOrUndeleteAlert('undelete') + else: + controlflow.invalid_argument_exit(argument, "gam undelete") + sys.exit(0) + elif command in ['close', 'reopen']: + # close and reopen will have to be split apart if either takes a new argument + argument = sys.argv[2].lower() + if argument in ['matter', 'vaultmatter']: + gapi_vault.updateMatter(action=command) + else: + controlflow.invalid_argument_exit(argument, f"gam {command}") + sys.exit(0) + elif command == 'print': + argument = sys.argv[2].lower().replace('-', '') + if argument == 'users': + doPrintUsers() + elif argument in ['nicknames', 'aliases']: + doPrintAliases() + elif argument == 'groups': + doPrintGroups() + elif argument in ['groupmembers', 'groupsmembers']: + doPrintGroupMembers() + elif argument in ['orgs', 'ous']: + doPrintOrgs() + elif argument == 'resources': + gapi_directory_resource.printResourceCalendars() + elif argument == 'cros': + gapi_directory_cros.doPrintCrosDevices() + elif argument == 'crosactivity': + gapi_directory_cros.doPrintCrosActivity() + elif argument == 'mobile': + doPrintMobileDevices() + elif argument in ['license', 'licenses', 'licence', 'licences']: + doPrintLicenses() + elif argument in ['token', 'tokens', 'oauth', '3lo']: + printShowTokens(3, None, None, True) + elif argument in ['schema', 'schemas']: + doPrintShowUserSchemas(True) + elif argument in ['courses', 'classes']: + doPrintCourses() + elif argument in ['courseparticipants', 'classparticipants']: + doPrintCourseParticipants() + elif argument == 'printers': + doPrintPrinters() + elif argument == 'printjobs': + doPrintPrintJobs() + elif argument in ['transfers', 'datatransfers']: + doPrintDataTransfers() + elif argument == 'transferapps': + doPrintTransferApps() + elif argument == 'domains': + doPrintDomains() + elif argument in ['domainaliases', 'aliasdomains']: + doPrintDomainAliases() + elif argument == 'admins': + doPrintAdmins() + elif argument in ['roles', 'adminroles']: + doPrintAdminRoles() + elif argument in ['guardian', 'guardians']: + doPrintShowGuardians(True) + elif argument in ['matters', 'vaultmatters']: + gapi_vault.printMatters() + elif argument in ['holds', 'vaultholds']: + gapi_vault.printHolds() + elif argument in ['exports', 'vaultexports']: + gapi_vault.printExports() + elif argument in ['building', 'buildings']: + gapi_directory_resource.printBuildings() + elif argument in ['feature', 'features']: + gapi_directory_resource.printFeatures() + elif argument in ['project', 'projects']: + doPrintShowProjects(True) + elif argument in ['alert', 'alerts']: + doPrintShowAlerts() + elif argument in ['alertfeedback', 'alertsfeedback']: + doPrintShowAlertFeedback() + else: + controlflow.invalid_argument_exit(argument, "gam print") + sys.exit(0) + elif command == 'show': + argument = sys.argv[2].lower() + if argument in ['schema', 'schemas']: + doPrintShowUserSchemas(False) + elif argument in ['guardian', 'guardians']: + doPrintShowGuardians(False) + elif argument in ['license', 'licenses', 'licence', 'licences']: + doShowLicenses() + elif argument in ['project', 'projects']: + doPrintShowProjects(False) + elif argument in ['sakey', 'sakeys']: + doShowServiceAccountKeys() + else: + controlflow.invalid_argument_exit(argument, "gam show") + sys.exit(0) + elif command in ['oauth', 'oauth2']: + argument = sys.argv[2].lower() + if argument in ['request', 'create']: + try: + login_hint = sys.argv[3].strip() + except IndexError: + login_hint = None + doRequestOAuth(login_hint) + elif argument in ['info', 'verify']: + OAuthInfo() + elif argument in ['delete', 'revoke']: + doDeleteOAuth() + elif argument in ['refresh']: + creds = getValidOauth2TxtCredentials(force_refresh=True) + if not creds: + controlflow.system_error_exit(5, 'Credential refresh failed') + else: + print('Credentials refreshed') + else: + controlflow.invalid_argument_exit(argument, "gam oauth") + sys.exit(0) + elif command == 'calendar': + argument = sys.argv[3].lower() + if argument == 'showacl': + gapi_calendar.printShowACLs(False) + elif argument == 'printacl': + gapi_calendar.printShowACLs(True) + elif argument == 'add': + gapi_calendar.addACL('Add') + elif argument in ['del', 'delete']: + gapi_calendar.delACL() + elif argument == 'update': + gapi_calendar.addACL('Update') + elif argument == 'wipe': + gapi_calendar.wipeData() + elif argument == 'addevent': + gapi_calendar.addOrUpdateEvent('add') + elif argument == 'updateevent': + gapi_calendar.addOrUpdateEvent('update') + elif argument == 'infoevent': + gapi_calendar.infoEvent() + elif argument == 'deleteevent': + gapi_calendar.moveOrDeleteEvent('delete') + elif argument == 'moveevent': + gapi_calendar.moveOrDeleteEvent('move') + elif argument == 'printevents': + gapi_calendar.printEvents() + elif argument == 'modify': + gapi_calendar.modifySettings() + else: + controlflow.invalid_argument_exit(argument, "gam calendar") + sys.exit(0) + elif command == 'printer': + if sys.argv[2].lower() == 'register': + doPrinterRegister() + sys.exit(0) + argument = sys.argv[3].lower() + if argument == 'showacl': + doPrinterShowACL() + elif argument == 'add': + doPrinterAddACL() + elif argument in ['del', 'delete', 'remove']: + doPrinterDelACL() + else: + controlflow.invalid_argument_exit(argument, "gam printer...") + sys.exit(0) + elif command == 'printjob': + argument = sys.argv[3].lower() + if argument == 'delete': + doDeletePrintJob() + elif argument == 'cancel': + doCancelPrintJob() + elif argument == 'submit': + doPrintJobSubmit() + elif argument == 'fetch': + doPrintJobFetch() + elif argument == 'resubmit': + doPrintJobResubmit() + else: + controlflow.invalid_argument_exit(argument, "gam printjob") + sys.exit(0) + elif command == 'report': + gapi_reports.showReport() + sys.exit(0) + elif command == 'whatis': + doWhatIs() + sys.exit(0) + elif command in ['course', 'class']: + argument = sys.argv[3].lower() + if argument in ['add', 'create']: + doAddCourseParticipant() + elif argument in ['del', 'delete', 'remove']: + doDelCourseParticipant() + elif argument == 'sync': + doSyncCourseParticipants() + else: + controlflow.invalid_argument_exit(argument, "gam course") + sys.exit(0) + elif command == 'download': + argument = sys.argv[2].lower() + if argument in ['export', 'vaultexport']: + gapi_vault.downloadExport() + elif argument in ['storagebucket']: + gapi_storage.download_bucket() + else: + controlflow.invalid_argument_exit(argument, "gam download") + sys.exit(0) + elif command == 'rotate': + argument = sys.argv[2].lower() + if argument in ['sakey', 'sakeys']: + doCreateOrRotateServiceAccountKeys() + else: + controlflow.invalid_argument_exit(argument, "gam rotate") + sys.exit(0) + users = getUsersToModify() + command = sys.argv[3].lower() + if command == 'print' and len(sys.argv) == 4: + for user in users: + print(user) + sys.exit(0) + if (GC_Values[GC_AUTO_BATCH_MIN] > 0) and (len(users) > GC_Values[GC_AUTO_BATCH_MIN]): + runCmdForUsers(None, users, True) + if command == 'transfer': + transferWhat = sys.argv[4].lower() + if transferWhat == 'drive': + transferDriveFiles(users) + elif transferWhat == 'seccals': + gapi_calendar.transferSecCals(users) + else: + controlflow.invalid_argument_exit(transferWhat, "gam transfer") + elif command == 'show': + showWhat = sys.argv[4].lower() + if showWhat in ['labels', 'label']: + showLabels(users) + elif showWhat == 'profile': + showProfile(users) + elif showWhat == 'calendars': + gapi_calendar.printShowCalendars(users, False) + elif showWhat == 'calsettings': + gapi_calendar.showCalSettings(users) + elif showWhat == 'drivesettings': + printDriveSettings(users) + elif showWhat == 'teamdrivethemes': + getTeamDriveThemes(users) + elif showWhat == 'drivefileacl': + showDriveFileACL(users) + elif showWhat == 'filelist': + printDriveFileList(users) + elif showWhat == 'filetree': + showDriveFileTree(users) + elif showWhat == 'fileinfo': + showDriveFileInfo(users) + elif showWhat == 'filerevisions': + showDriveFileRevisions(users) + elif showWhat == 'sendas': + printShowSendAs(users, False) + elif showWhat == 'smime': + printShowSmime(users, False) + elif showWhat == 'gmailprofile': + showGmailProfile(users) + elif showWhat in ['sig', 'signature']: + getSignature(users) + elif showWhat == 'forward': + printShowForward(users, False) + elif showWhat in ['pop', 'pop3']: + getPop(users) + elif showWhat in ['imap', 'imap4']: + getImap(users) + elif showWhat in ['language']: + getLanguage(users) + elif showWhat == 'vacation': + getVacation(users) + elif showWhat in ['delegate', 'delegates']: + printShowDelegates(users, False) + elif showWhat in ['backupcode', 'backupcodes', 'verificationcodes']: + doGetBackupCodes(users) + elif showWhat in ['asp', 'asps', 'applicationspecificpasswords']: + doGetASPs(users) + elif showWhat in ['token', 'tokens', 'oauth', '3lo']: + printShowTokens(5, 'users', users, False) + elif showWhat == 'driveactivity': + printDriveActivity(users) + elif showWhat in ['filter', 'filters']: + printShowFilters(users, False) + elif showWhat in ['forwardingaddress', 'forwardingaddresses']: + printShowForwardingAddresses(users, False) + elif showWhat in ['teamdrive', 'teamdrives']: + printShowTeamDrives(users, False) + elif showWhat in ['teamdriveinfo']: + doGetTeamDriveInfo(users) + else: + controlflow.invalid_argument_exit(showWhat, "gam show") + elif command == 'print': + printWhat = sys.argv[4].lower() + if printWhat == 'calendars': + gapi_calendar.printShowCalendars(users, True) + elif printWhat in ['delegate', 'delegates']: + printShowDelegates(users, True) + elif printWhat == 'driveactivity': + printDriveActivity(users) + elif printWhat == 'drivesettings': + printDriveSettings(users) + elif printWhat == 'filelist': + printDriveFileList(users) + elif printWhat in ['filter', 'filters']: + printShowFilters(users, True) + elif printWhat == 'forward': + printShowForward(users, True) + elif printWhat in ['forwardingaddress', 'forwardingaddresses']: + printShowForwardingAddresses(users, True) + elif printWhat == 'sendas': + printShowSendAs(users, True) + elif printWhat == 'smime': + printShowSmime(users, True) + elif printWhat in ['token', 'tokens', 'oauth', '3lo']: + printShowTokens(5, 'users', users, True) + elif printWhat in ['teamdrive', 'teamdrives']: + printShowTeamDrives(users, True) + else: + controlflow.invalid_argument_exit(printWhat, "gam print") + elif command == 'modify': + modifyWhat = sys.argv[4].lower() + if modifyWhat in ['message', 'messages']: + doProcessMessagesOrThreads(users, 'modify', 'messages') + elif modifyWhat in ['thread', 'threads']: + doProcessMessagesOrThreads(users, 'modify', 'threads') + else: + controlflow.invalid_argument_exit(modifyWhat, "gam modify") + elif command == 'trash': + trashWhat = sys.argv[4].lower() + if trashWhat in ['message', 'messages']: + doProcessMessagesOrThreads(users, 'trash', 'messages') + elif trashWhat in ['thread', 'threads']: + doProcessMessagesOrThreads(users, 'trash', 'threads') + else: + controlflow.invalid_argument_exit(trashWhat, "gam trash") + elif command == 'untrash': + untrashWhat = sys.argv[4].lower() + if untrashWhat in ['message', 'messages']: + doProcessMessagesOrThreads(users, 'untrash', 'messages') + elif untrashWhat in ['thread', 'threads']: + doProcessMessagesOrThreads(users, 'untrash', 'threads') + else: + controlflow.invalid_argument_exit(untrashWhat, "gam untrash") + elif command in ['delete', 'del']: + delWhat = sys.argv[4].lower() + if delWhat == 'delegate': + deleteDelegate(users) + elif delWhat == 'calendar': + gapi_calendar.deleteCalendar(users) + elif delWhat in ['labels', 'label']: + doDeleteLabel(users) + elif delWhat in ['message', 'messages']: + runCmdForUsers(doProcessMessagesOrThreads, users, default_to_batch=True, function='delete', unit='messages') + elif delWhat in ['thread', 'threads']: + runCmdForUsers(doProcessMessagesOrThreads, users, default_to_batch=True, function='delete', unit='threads') + elif delWhat == 'photo': + deletePhoto(users) + elif delWhat in ['license', 'licence']: + doLicense(users, 'delete') + elif delWhat in ['backupcode', 'backupcodes', 'verificationcodes']: + doDelBackupCodes(users) + elif delWhat in ['asp', 'asps', 'applicationspecificpasswords']: + doDelASP(users) + elif delWhat in ['token', 'tokens', 'oauth', '3lo']: + doDelTokens(users) + elif delWhat in ['group', 'groups']: + deleteUserFromGroups(users) + elif delWhat in ['alias', 'aliases']: + doRemoveUsersAliases(users) + elif delWhat == 'emptydrivefolders': + deleteEmptyDriveFolders(users) + elif delWhat == 'drivefile': + deleteDriveFile(users) + elif delWhat in ['drivefileacl', 'drivefileacls']: + delDriveFileACL(users) + elif delWhat in ['filter', 'filters']: + deleteFilters(users) + elif delWhat in ['forwardingaddress', 'forwardingaddresses']: + deleteForwardingAddresses(users) + elif delWhat == 'sendas': + deleteSendAs(users) + elif delWhat == 'smime': + deleteSmime(users) + elif delWhat == 'teamdrive': + doDeleteTeamDrive(users) + else: + controlflow.invalid_argument_exit(delWhat, "gam delete") + elif command in ['add', 'create']: + addWhat = sys.argv[4].lower() + if addWhat == 'calendar': + if command == 'add': + gapi_calendar.addCalendar(users) + else: + controlflow.system_error_exit(2, f'{addWhat} is not implemented for "gam {command}"') + elif addWhat == 'drivefile': + createDriveFile(users) + elif addWhat in ['license', 'licence']: + doLicense(users, 'insert') + elif addWhat in ['drivefileacl', 'drivefileacls']: + addDriveFileACL(users) + elif addWhat in ['label', 'labels']: + doLabel(users, 5) + elif addWhat in ['delegate', 'delegates']: + addDelegates(users, 5) + elif addWhat in ['filter', 'filters']: + addFilter(users, 5) + elif addWhat in ['forwardingaddress', 'forwardingaddresses']: + addForwardingAddresses(users) + elif addWhat == 'sendas': + addUpdateSendAs(users, 5, True) + elif addWhat == 'smime': + addSmime(users) + elif addWhat == 'teamdrive': + doCreateTeamDrive(users) + else: + controlflow.invalid_argument_exit(addWhat, f"gam {command}") + elif command == 'update': + updateWhat = sys.argv[4].lower() + if updateWhat == 'calendar': + gapi_calendar.updateCalendar(users) + elif updateWhat == 'calattendees': + gapi_calendar.changeAttendees(users) + elif updateWhat == 'photo': + doPhoto(users) + elif updateWhat in ['license', 'licence']: + doLicense(users, 'patch') + elif updateWhat == 'user': + doUpdateUser(users, 5) + elif updateWhat in ['backupcode', 'backupcodes', 'verificationcodes']: + doGenBackupCodes(users) + elif updateWhat == 'drivefile': + doUpdateDriveFile(users) + elif updateWhat in ['drivefileacls', 'drivefileacl']: + updateDriveFileACL(users) + elif updateWhat in ['label', 'labels']: + renameLabels(users) + elif updateWhat == 'labelsettings': + updateLabels(users) + elif updateWhat == 'sendas': + addUpdateSendAs(users, 5, False) + elif updateWhat == 'smime': + updateSmime(users) + elif updateWhat == 'teamdrive': + doUpdateTeamDrive(users) + else: + controlflow.invalid_argument_exit(updateWhat, "gam update") + elif command in ['deprov', 'deprovision']: + doDeprovUser(users) + elif command == 'get': + getWhat = sys.argv[4].lower() + if getWhat == 'photo': + getPhoto(users) + elif getWhat == 'drivefile': + downloadDriveFile(users) + else: + controlflow.invalid_argument_exit(getWhat, "gam get") + elif command == 'empty': + emptyWhat = sys.argv[4].lower() + if emptyWhat == 'drivetrash': + doEmptyDriveTrash(users) + else: + controlflow.invalid_argument_exit(emptyWhat, "gam empty") + elif command == 'info': + infoWhat = sys.argv[4].lower() + if infoWhat == 'calendar': + gapi_calendar.infoCalendar(users) + elif infoWhat in ['filter', 'filters']: + infoFilters(users) + elif infoWhat in ['forwardingaddress', 'forwardingaddresses']: + infoForwardingAddresses(users) + elif infoWhat == 'sendas': + infoSendAs(users) + else: + controlflow.invalid_argument_exit(infoWhat, "gam info") + elif command == 'check': + checkWhat = sys.argv[4].replace('_', '').lower() + if checkWhat == 'serviceaccount': + doCheckServiceAccount(users) + else: + controlflow.invalid_argument_exit(checkWhat, "gam check") + elif command == 'profile': + doProfile(users) + elif command == 'imap': + #doImap(users) + runCmdForUsers(doImap, users, default_to_batch=True) + elif command == 'sendemail': + sendOrDropEmail(users, 'send') + elif command == 'importemail': + sendOrDropEmail(users, 'import') + elif command == 'insertemail': + sendOrDropEmail(users, 'insert') + elif command == 'draftemail': + sendOrDropEmail(users, 'draft') + elif command == 'language': + doLanguage(users) + elif command in ['pop', 'pop3']: + doPop(users) + elif command == 'sendas': + addUpdateSendAs(users, 4, True) + elif command == 'label': + doLabel(users, 4) + elif command == 'filter': + addFilter(users, 4) + elif command == 'forward': + doForward(users) + elif command in ['sig', 'signature']: + doSignature(users) + elif command == 'vacation': + doVacation(users) + elif command in ['delegate', 'delegates']: + addDelegates(users, 4) + elif command == 'watch': + if len(sys.argv) > 4: + watchWhat = sys.argv[4].lower() + else: + watchWhat = 'gmail' + if watchWhat == 'gmail': + watchGmail(users) + else: + controlflow.invalid_argument_exit(watchWhat, "gam watch") + else: + controlflow.invalid_argument_exit(command, "gam") + except IndexError: + showUsage() + sys.exit(2) + except KeyboardInterrupt: + sys.exit(50) + except socket.error as e: + controlflow.system_error_exit(3, str(e)) + except MemoryError: + controlflow.system_error_exit(99, MESSAGE_GAM_OUT_OF_MEMORY) + except SystemExit as e: + GM_Globals[GM_SYSEXITRC] = e.code + return GM_Globals[GM_SYSEXITRC] diff --git a/src/gam/__main__.py b/src/gam/__main__.py new file mode 100644 index 00000000..0d15fdfd --- /dev/null +++ b/src/gam/__main__.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# GAM +# +# Copyright 2019, LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""GAM is a command line tool which allows Administrators to control their G Suite domain and accounts. + +With GAM you can programmatically create users, turn on/off services for users like POP and Forwarding and much more. +For more information, see https://git.io/gam +""" + +import sys +from multiprocessing import freeze_support +from multiprocessing import set_start_method + +from gam import controlflow +import gam + +def main(argv): + freeze_support() + if sys.platform == 'darwin': + # https://bugs.python.org/issue33725 in Python 3.8.0 seems + # to break parallel operations with errors about extra -b + # command line arguments + set_start_method('fork') + if sys.version_info[0] < 3 or sys.version_info[1] < 6: + controlflow.system_error_exit(5, + f'GAM requires Python 3.6 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.' % sys.version_info[ + :3]) + sys.exit(gam.ProcessGAMCommand(sys.argv)) + +# Run from command line +if __name__ == "__main__": + main(sys.argv) diff --git a/src/auth/__init__.py b/src/gam/auth/__init__.py similarity index 87% rename from src/auth/__init__.py rename to src/gam/auth/__init__.py index bf5bec1d..9cd50320 100644 --- a/src/auth/__init__.py +++ b/src/gam/auth/__init__.py @@ -1,10 +1,10 @@ """Authentication/Credentials general purpose and convenience methods.""" -import transport -from var import _FN_OAUTH2_TXT -from var import GC_OAUTH2_TXT -from var import GC_Values -from . import oauth +from gam.auth import oauth +from gam.var import _FN_OAUTH2_TXT +from gam.var import GC_OAUTH2_TXT +from gam.var import GC_Values + # TODO: Move logic that determines file name into this module. We should be able # to discover the file location without accessing a private member or waiting # for a global initialization. diff --git a/src/auth/oauth.py b/src/gam/auth/oauth.py similarity index 99% rename from src/auth/oauth.py rename to src/gam/auth/oauth.py index 9a733740..b54a9907 100644 --- a/src/auth/oauth.py +++ b/src/gam/auth/oauth.py @@ -12,12 +12,11 @@ import google_auth_oauthlib.flow import google.oauth2.credentials import google.oauth2.id_token -import fileutils -import transport -from var import GAM_INFO -from var import GM_Globals -from var import GM_WINDOWS -import utils +from gam import fileutils +from gam import transport +from gam.var import GM_Globals +from gam.var import GM_WINDOWS +from gam import utils MESSAGE_CONSOLE_AUTHORIZATION_PROMPT = ('\nGo to the following link in your ' 'browser:\n\n\t{url}\n') diff --git a/src/auth/oauth_test.py b/src/gam/auth/oauth_test.py similarity index 99% rename from src/auth/oauth_test.py rename to src/gam/auth/oauth_test.py index 40721609..fd35b923 100644 --- a/src/auth/oauth_test.py +++ b/src/gam/auth/oauth_test.py @@ -10,7 +10,7 @@ from unittest.mock import patch import google.oauth2.credentials -from auth import oauth +from gam.auth import oauth class CredentialsTest(unittest.TestCase): diff --git a/src/controlflow.py b/src/gam/controlflow.py similarity index 76% rename from src/controlflow.py rename to src/gam/controlflow.py index dc5bc434..5fcb912a 100644 --- a/src/controlflow.py +++ b/src/gam/controlflow.py @@ -3,9 +3,9 @@ import random import sys import time -import display # TODO: Change to relative import when gam is setup as a package -from var import MESSAGE_HEADER_NOT_FOUND_IN_CSV_HEADERS -from var import MESSAGE_INVALID_JSON +from gam import display +from gam.var import MESSAGE_HEADER_NOT_FOUND_IN_CSV_HEADERS +from gam.var import MESSAGE_INVALID_JSON def system_error_exit(return_code, message): @@ -21,38 +21,35 @@ def system_error_exit(return_code, message): def invalid_argument_exit(argument, command): - '''Indicate that the argument is not valid for the command. + """Indicate that the argument is not valid for the command. Args: argument: the invalid argument command: the base GAM command - ''' - system_error_exit( - 2, - f'{argument} is not a valid argument for "{command}"') + """ + system_error_exit(2, f'{argument} is not a valid argument for "{command}"') + def missing_argument_exit(argument, command): - '''Indicate that the argument is missing for the command. + """Indicate that the argument is missing for the command. Args: argument: the missingagrument command: the base GAM command - ''' - system_error_exit( - 2, - f'missing argument {argument} for "{command}"') + """ + system_error_exit(2, f'missing argument {argument} for "{command}"') + def expected_argument_exit(name, expected, argument): - '''Indicate that the argument does not have an expected value for the command. + """Indicate that the argument does not have an expected value for the command. Args: name: the field name expected: the expected values argument: the invalid argument - ''' - system_error_exit( - 2, - f'{name} must be one of {expected}; got {argument}') + """ + system_error_exit(2, f'{name} must be one of {expected}; got {argument}') + def csv_field_error_exit(field_name, field_names): """Raises a system exit when a CSV field is malformed. @@ -93,7 +90,7 @@ def wait_on_failure(current_attempt_num, 60) + float(random.randint(1, 1000)) / 1000 if current_attempt_num > error_print_threshold: sys.stderr.write((f'Temporary error: {error_message}, Backing off: ' - f'{int(wait_on_fail)} seconds, Retry: ' - f'{current_attempt_num}/{total_num_retries}\n')) + f'{int(wait_on_fail)} seconds, Retry: ' + f'{current_attempt_num}/{total_num_retries}\n')) sys.stderr.flush() time.sleep(wait_on_fail) diff --git a/src/controlflow_test.py b/src/gam/controlflow_test.py similarity index 99% rename from src/controlflow_test.py rename to src/gam/controlflow_test.py index e9ce0e66..cecc4dd3 100644 --- a/src/controlflow_test.py +++ b/src/gam/controlflow_test.py @@ -3,7 +3,7 @@ import unittest from unittest.mock import patch -import controlflow +from gam import controlflow class ControlFlowTest(unittest.TestCase): diff --git a/src/display.py b/src/gam/display.py similarity index 97% rename from src/display.py rename to src/gam/display.py index 0d7f2078..6e1b97fc 100644 --- a/src/display.py +++ b/src/gam/display.py @@ -10,10 +10,10 @@ import dateutil import googleapiclient.http #TODO: get rid of these hacks -import __main__ -from var import * -import controlflow -import gapi +import gam +from gam.var import * +from gam import controlflow +from gam import gapi def current_count(i, count): @@ -171,8 +171,8 @@ def write_csv_file(csvRows, titles, list_type, todrive): except IOError as e: controlflow.system_error_exit(6, e) if todrive: - admin_email = __main__._getValueFromOAuth('email') - _, drive = __main__.buildDrive3GAPIObject(admin_email) + admin_email = gam._getValueFromOAuth('email') + _, drive = gam.buildDrive3GAPIObject(admin_email) if not drive: print(f'''\nGAM is not authorized to create Drive files. Please run: gam user {admin_email} check serviceaccount @@ -200,7 +200,7 @@ and follow recommend steps to authorize GAM for Drive access.''') if GC_Values[GC_NO_BROWSER]: msg_txt = f'Drive file uploaded to:\n {file_url}' msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}' - __main__.send_email(msg_subj, msg_txt) + gam.send_email(msg_subj, msg_txt) print(msg_txt) else: webbrowser.open(file_url) diff --git a/src/display_test.py b/src/gam/display_test.py similarity index 95% rename from src/display_test.py rename to src/gam/display_test.py index 4dde2f70..3b95ca0d 100644 --- a/src/display_test.py +++ b/src/gam/display_test.py @@ -3,9 +3,9 @@ import unittest from unittest.mock import patch -import display -from var import ERROR_PREFIX -from var import WARNING_PREFIX +from gam import display +from gam.var import ERROR_PREFIX +from gam.var import WARNING_PREFIX class DisplayTest(unittest.TestCase): diff --git a/src/fileutils.py b/src/gam/fileutils.py similarity index 97% rename from src/fileutils.py rename to src/gam/fileutils.py index 64836118..0384ebd1 100644 --- a/src/fileutils.py +++ b/src/gam/fileutils.py @@ -4,11 +4,11 @@ import io import os import sys -import controlflow -import display -from var import GM_Globals -from var import GM_SYS_ENCODING -from var import UTF8_SIG +from gam import controlflow +from gam import display +from gam.var import GM_Globals +from gam.var import GM_SYS_ENCODING +from gam.var import UTF8_SIG def _open_file(filename, mode, encoding=None, newline=None): diff --git a/src/fileutils_test.py b/src/gam/fileutils_test.py similarity index 99% rename from src/fileutils_test.py rename to src/gam/fileutils_test.py index 5493071b..859a0399 100644 --- a/src/fileutils_test.py +++ b/src/gam/fileutils_test.py @@ -6,7 +6,7 @@ import unittest from unittest.mock import MagicMock from unittest.mock import patch -import fileutils +from gam import fileutils class FileutilsTest(unittest.TestCase): diff --git a/src/gapi/__init__.py b/src/gam/gapi/__init__.py similarity index 96% rename from src/gapi/__init__.py rename to src/gam/gapi/__init__.py index e2be542a..fa1009eb 100644 --- a/src/gapi/__init__.py +++ b/src/gam/gapi/__init__.py @@ -6,14 +6,14 @@ import googleapiclient.errors import google.auth.exceptions import httplib2 -import controlflow -import display -from gapi import errors -import transport -from var import (GM_Globals, GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER, - GM_EXTRA_ARGS_DICT, GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID, - MAX_RESULTS_API_EXCEPTIONS, MESSAGE_API_ACCESS_CONFIG, - MESSAGE_API_ACCESS_DENIED, MESSAGE_SERVICE_NOT_APPLICABLE) +from gam import controlflow +from gam import display +from gam.gapi import errors +from gam import transport +from gam.var import (GM_Globals, GM_CURRENT_API_SCOPES, GM_CURRENT_API_USER, + GM_EXTRA_ARGS_DICT, GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID, + MAX_RESULTS_API_EXCEPTIONS, MESSAGE_API_ACCESS_CONFIG, + MESSAGE_API_ACCESS_DENIED, MESSAGE_SERVICE_NOT_APPLICABLE) def call(service, diff --git a/src/gapi/__init___test.py b/src/gam/gapi/__init___test.py similarity index 99% rename from src/gapi/__init___test.py rename to src/gam/gapi/__init___test.py index cd1aae4d..5b3026a1 100644 --- a/src/gapi/__init___test.py +++ b/src/gam/gapi/__init___test.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock from unittest.mock import patch from gam import SetGlobalVariables -import gapi -from gapi import errors +import gam.gapi as gapi +from gam.gapi import errors def create_http_error(status, reason, message): diff --git a/src/gapi/calendar.py b/src/gam/gapi/calendar.py similarity index 95% rename from src/gapi/calendar.py rename to src/gam/gapi/calendar.py index f73551d3..4859e0a1 100644 --- a/src/gapi/calendar.py +++ b/src/gam/gapi/calendar.py @@ -3,28 +3,28 @@ import sys import uuid # TODO: get rid of these hacks -import __main__ -from var import * +import gam +from gam.var import * -import controlflow -import display -import fileutils -import gapi -import utils +from gam import controlflow +from gam import display +from gam import fileutils +from gam import gapi +from gam import utils def normalizeCalendarId(calname, checkPrimary=False): if checkPrimary and calname.lower() == 'primary': return calname if not GC_Values[GC_DOMAIN]: - GC_Values[GC_DOMAIN] = __main__._getValueFromOAuth('hd') - return __main__.convertUIDtoEmailAddress(calname, + GC_Values[GC_DOMAIN] = gam._getValueFromOAuth('hd') + return gam.convertUIDtoEmailAddress(calname, email_types=['user', 'resource']) def buildCalendarGAPIObject(calname): calendarId = normalizeCalendarId(calname) - return (calendarId, __main__.buildGAPIServiceObject('calendar', + return (calendarId, gam.buildGAPIServiceObject('calendar', calendarId)) @@ -36,9 +36,9 @@ def buildCalendarDataGAPIObject(calname): # so we need to access them as the admin. cal = None if not calname.endswith('.calendar.google.com'): - cal = __main__.buildGAPIServiceObject('calendar', calendarId, False) + cal = gam.buildGAPIServiceObject('calendar', calendarId, False) if cal is None: - _, cal = buildCalendarGAPIObject(__main__._getValueFromOAuth('email')) + _, cal = buildCalendarGAPIObject(gam._getValueFromOAuth('email')) return (calendarId, cal) def printShowACLs(csvFormat): @@ -87,7 +87,7 @@ def _getCalendarACLScope(i, body): body['scope']['type'] = myarg i += 1 if myarg in ['user', 'group']: - body['scope']['value'] = __main__.normalizeEmailAddressOrUID( + body['scope']['value'] = gam.normalizeEmailAddressOrUID( sys.argv[i], noUid=True) i += 1 elif myarg == 'domain': @@ -99,7 +99,7 @@ def _getCalendarACLScope(i, body): body['scope']['value'] = GC_Values[GC_DOMAIN] elif myarg != 'default': body['scope']['type'] = 'user' - body['scope']['value'] = __main__.normalizeEmailAddressOrUID( + body['scope']['value'] = gam.normalizeEmailAddressOrUID( myarg, noUid=True) return i @@ -130,7 +130,7 @@ def addACL(function): while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'sendnotifications': - sendNotifications = __main__.getBoolean(sys.argv[i+1], myarg) + sendNotifications = gam.getBoolean(sys.argv[i+1], myarg) i += 2 else: controlflow.invalid_argument_exit( @@ -237,7 +237,7 @@ def getSendUpdates(myarg, i, cal): sendUpdates = 'all' i += 1 elif myarg == 'sendnotifications': - sendUpdates = 'all' if __main__.getBoolean(sys.argv[i+1], myarg) else 'none' + sendUpdates = 'all' if gam.getBoolean(sys.argv[i+1], myarg) else 'none' i += 2 else: # 'sendupdates': sendUpdatesMap = {} @@ -393,18 +393,18 @@ def getEventAttributes(i, calendarId, cal, body, action): body['guestsCanInviteOthers'] = False i += 1 elif myarg == 'guestscaninviteothers': - body['guestsCanInviteTohters'] = __main__.getBoolean( + body['guestsCanInviteTohters'] = gam.getBoolean( sys.argv[i+1], 'guestscaninviteothers') i += 2 elif myarg == 'guestscantseeothers': body['guestsCanSeeOtherGuests'] = False i += 1 elif myarg == 'guestscanseeothers': - body['guestsCanSeeOtherGuests'] = __main__.getBoolean( + body['guestsCanSeeOtherGuests'] = gam.getBoolean( sys.argv[i+1], 'guestscanseeothers') i += 2 elif myarg == 'guestscanmodify': - body['guestsCanModify'] = __main__.getBoolean( + body['guestsCanModify'] = gam.getBoolean( sys.argv[i+1], 'guestscanmodify') i += 2 elif myarg == 'id': @@ -458,7 +458,7 @@ def getEventAttributes(i, calendarId, cal, body, action): i += 1 elif myarg == 'reminder': minutes = \ - __main__.getInteger(sys.argv[i+1], myarg, minVal=0, + gam.getInteger(sys.argv[i+1], myarg, minVal=0, maxVal=CALENDAR_REMINDER_MAX_MINUTES) reminder = {'minutes': minutes, 'method': sys.argv[i+2]} body.setdefault( @@ -483,7 +483,7 @@ def getEventAttributes(i, calendarId, cal, body, action): body['extendedProperties']['shared'][sys.argv[i+1]] = sys.argv[i+2] i += 3 elif myarg == 'colorindex': - body['colorId'] = __main__.getInteger( + body['colorId'] = gam.getInteger( sys.argv[i+1], myarg, CALENDAR_EVENT_MIN_COLOR_INDEX, CALENDAR_EVENT_MAX_COLOR_INDEX) i += 2 @@ -649,25 +649,25 @@ def getCalendarAttributes(i, body, function): while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'selected': - body['selected'] = __main__.getBoolean(sys.argv[i+1], myarg) + body['selected'] = gam.getBoolean(sys.argv[i+1], myarg) i += 2 elif myarg == 'hidden': - body['hidden'] = __main__.getBoolean(sys.argv[i+1], myarg) + body['hidden'] = gam.getBoolean(sys.argv[i+1], myarg) i += 2 elif myarg == 'summary': body['summaryOverride'] = sys.argv[i+1] i += 2 elif myarg == 'colorindex': - body['colorId'] = __main__.getInteger( + body['colorId'] = gam.getInteger( sys.argv[i+1], myarg, minVal=CALENDAR_MIN_COLOR_INDEX, maxVal=CALENDAR_MAX_COLOR_INDEX) i += 2 elif myarg == 'backgroundcolor': - body['backgroundColor'] = __main__.getColor(sys.argv[i+1]) + body['backgroundColor'] = gam.getColor(sys.argv[i+1]) colorRgbFormat = True i += 2 elif myarg == 'foregroundcolor': - body['foregroundColor'] = __main__.getColor(sys.argv[i+1]) + body['foregroundColor'] = gam.getColor(sys.argv[i+1]) colorRgbFormat = True i += 2 elif myarg == 'reminder': @@ -677,7 +677,7 @@ def getCalendarAttributes(i, body, function): if method not in CALENDAR_REMINDER_METHODS: controlflow.expected_argument_exit("Method", ", ".join( CALENDAR_REMINDER_METHODS+CLEAR_NONE_ARGUMENT), method) - minutes = __main__.getInteger( + minutes = gam.getInteger( sys.argv[i+2], myarg, minVal=0, maxVal=CALENDAR_REMINDER_MAX_MINUTES) body['defaultReminders'].append( @@ -862,7 +862,7 @@ def transferSecCals(users): remove_source_user = False i += 1 elif myarg == 'sendnotifications': - sendNotifications = __main__.getBoolean(sys.argv[i+1], myarg) + sendNotifications = gam.getBoolean(sys.argv[i+1], myarg) i += 2 else: controlflow.invalid_argument_exit( diff --git a/src/gam/gapi/directory/__init__.py b/src/gam/gapi/directory/__init__.py new file mode 100644 index 00000000..7627ecec --- /dev/null +++ b/src/gam/gapi/directory/__init__.py @@ -0,0 +1,5 @@ +import gam + + +def buildGAPIObject(): + return gam.buildGAPIObject('directory') diff --git a/src/gapi/directory/cros.py b/src/gam/gapi/directory/cros.py similarity index 96% rename from src/gapi/directory/cros.py rename to src/gam/gapi/directory/cros.py index c2e904a4..36554806 100644 --- a/src/gapi/directory/cros.py +++ b/src/gam/gapi/directory/cros.py @@ -1,17 +1,17 @@ import datetime -from var import * -import __main__ -import controlflow -import display -import fileutils -import gapi -import gapi.directory -import utils +from gam.var import * +import gam +from gam import controlflow +from gam import display +from gam import fileutils +from gam import gapi +from gam.gapi import directory as gapi_directory +from gam import utils def doUpdateCros(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() i, devices = getCrOSDeviceEntity(3, cd) update_body = {} action_body = {} @@ -32,7 +32,7 @@ def doUpdateCros(): update_body['annotatedAssetId'] = sys.argv[i+1] i += 2 elif myarg in ['ou', 'org']: - orgUnitPath = __main__.getOrgUnitItem(sys.argv[i+1]) + orgUnitPath = gam.getOrgUnitItem(sys.argv[i+1]) i += 2 elif myarg == 'action': action = sys.argv[i+1].lower().replace('_', '').replace('-', '') @@ -84,7 +84,7 @@ def doUpdateCros(): sys.exit(3) for deviceId in devices: i += 1 - cur_count = __main__.currentCount(i, count) + cur_count = gam.currentCount(i, count) print(f' performing action {action} for {deviceId}{cur_count}') gapi.call(cd.chromeosdevices(), function='action', customerId=GC_Values[GC_CUSTOMER_ID], @@ -93,7 +93,7 @@ def doUpdateCros(): if update_body: for deviceId in devices: i += 1 - current_count = __main__.currentCount(i, count) + current_count = gam.currentCount(i, count) print(f' updating {deviceId}{current_count}') gapi.call(cd.chromeosdevices(), 'update', customerId=GC_Values[GC_CUSTOMER_ID], @@ -110,7 +110,7 @@ def doUpdateCros(): def doGetCrosInfo(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() i, devices = getCrOSDeviceEntity(3, cd) downloadfile = None targetFolder = GC_Values[GC_DRIVE_DIR] @@ -125,7 +125,7 @@ def doGetCrosInfo(): noLists = True i += 1 elif myarg == 'listlimit': - listLimit = __main__.getInteger(sys.argv[i+1], myarg, minVal=-1) + listLimit = gam.getInteger(sys.argv[i+1], myarg, minVal=-1) i += 2 elif myarg in CROS_START_ARGUMENTS: startDate = _getFilterDate(sys.argv[i+1]) @@ -318,7 +318,7 @@ def doGetCrosInfo(): def doPrintCrosActivity(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() todrive = False titles = ['deviceId', 'annotatedAssetId', 'annotatedLocation', 'serialNumber', 'orgUnitPath'] @@ -335,10 +335,10 @@ def doPrintCrosActivity(): while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg in ['query', 'queries']: - queries = __main__.getQueries(myarg, sys.argv[i+1]) + queries = gam.getQueries(myarg, sys.argv[i+1]) i += 2 elif myarg == 'limittoou': - orgUnitPath = __main__.getOrgUnitItem(sys.argv[i+1]) + orgUnitPath = gam.getOrgUnitItem(sys.argv[i+1]) i += 2 elif myarg == 'todrive': todrive = True @@ -366,7 +366,7 @@ def doPrintCrosActivity(): endDate = _getFilterDate(sys.argv[i+1]) i += 2 elif myarg == 'listlimit': - listLimit = __main__.getInteger(sys.argv[i+1], myarg, minVal=0) + listLimit = gam.getInteger(sys.argv[i+1], myarg, minVal=0) i += 2 elif myarg == 'delimiter': delimiter = sys.argv[i+1] @@ -393,7 +393,7 @@ def doPrintCrosActivity(): display.add_titles_to_csv_file(titles_to_add, titles) fields = f'nextPageToken,chromeosdevices({",".join(fieldsList)})' for query in queries: - __main__.printGettingAllItems('CrOS Devices', query) + gam.printGettingAllItems('CrOS Devices', query) page_message = gapi.got_total_items_msg('CrOS Devices', '...\n') all_cros = gapi.get_all_pages(cd.chromeosdevices(), 'list', 'chromeosdevices', @@ -479,7 +479,7 @@ def doPrintCrosDevices(): elif myarg in CROS_SYSTEM_RAM_FREE_REPORTS_ARGUMENTS: selectedLists['systemRamFreeReports'] = True - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() todrive = False fieldsList = [] fieldsTitles = {} @@ -497,10 +497,10 @@ def doPrintCrosDevices(): while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg in ['query', 'queries']: - queries = __main__.getQueries(myarg, sys.argv[i+1]) + queries = gam.getQueries(myarg, sys.argv[i+1]) i += 2 elif myarg == 'limittoou': - orgUnitPath = __main__.getOrgUnitItem(sys.argv[i+1]) + orgUnitPath = gam.getOrgUnitItem(sys.argv[i+1]) i += 2 elif myarg == 'todrive': todrive = True @@ -510,7 +510,7 @@ def doPrintCrosDevices(): selectedLists = {} i += 1 elif myarg == 'listlimit': - listLimit = __main__.getInteger(sys.argv[i+1], myarg, minVal=0) + listLimit = gam.getInteger(sys.argv[i+1], myarg, minVal=0) i += 2 elif myarg in CROS_START_ARGUMENTS: startDate = _getFilterDate(sys.argv[i+1]) @@ -589,7 +589,7 @@ def doPrintCrosDevices(): else: fields = None for query in queries: - __main__.printGettingAllItems('CrOS Devices', query) + gam.printGettingAllItems('CrOS Devices', query) page_message = gapi.got_total_items_msg('CrOS Devices', '...\n') all_cros = gapi.get_all_pages(cd.chromeosdevices(), 'list', 'chromeosdevices', @@ -742,9 +742,9 @@ def doPrintCrosDevices(): def getCrOSDeviceEntity(i, cd): myarg = sys.argv[i].lower() if myarg == 'cros_sn': - return i+2, __main__.getUsersToModify('cros_sn', sys.argv[i+1]) + return i+2, gam.getUsersToModify('cros_sn', sys.argv[i+1]) if myarg == 'query': - return i+2, __main__.getUsersToModify('crosquery', sys.argv[i+1]) + return i+2, gam.getUsersToModify('crosquery', sys.argv[i+1]) if myarg[:6] == 'query:': query = sys.argv[i][6:] if query[:12].lower() == 'orgunitpath:': diff --git a/src/gapi/directory/customer.py b/src/gam/gapi/directory/customer.py similarity index 93% rename from src/gapi/directory/customer.py rename to src/gam/gapi/directory/customer.py index ee6d66bb..89411175 100644 --- a/src/gapi/directory/customer.py +++ b/src/gam/gapi/directory/customer.py @@ -1,14 +1,14 @@ import datetime -from var import * -import controlflow -import gapi -import gapi.directory -import gapi.reports +from gam.var import * +from gam import controlflow +from gam import gapi +from gam.gapi import directory as gapi_directory +from gam.gapi import reports as gapi_reports def doGetCustomerInfo(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() customer_info = gapi.call(cd.customers(), 'get', customerKey=GC_Values[GC_CUSTOMER_ID]) print(f'Customer ID: {customer_info["id"]}') @@ -59,7 +59,7 @@ def doGetCustomerInfo(): customerId = GC_Values[GC_CUSTOMER_ID] if customerId == MY_CUSTOMER: customerId = None - rep = gapi.reports.buildGAPIObject() + rep = gapi_reports.buildGAPIObject() usage = None throw_reasons = [gapi.errors.ErrorReason.INVALID] while True: @@ -71,7 +71,7 @@ def doGetCustomerInfo(): parameters=parameters) break except gapi.errors.GapiInvalidError as e: - tryDate = gapi.reports._adjust_date(str(e)) + tryDate = gapi_reports._adjust_date(str(e)) if not usage: print('No user count data available.') return @@ -84,7 +84,7 @@ def doGetCustomerInfo(): def doUpdateCustomer(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() body = {} i = 3 while i < len(sys.argv): diff --git a/src/gapi/directory/resource.py b/src/gam/gapi/directory/resource.py similarity index 95% rename from src/gapi/directory/resource.py rename to src/gam/gapi/directory/resource.py index a5c9a286..8b52139d 100644 --- a/src/gapi/directory/resource.py +++ b/src/gam/gapi/directory/resource.py @@ -1,17 +1,18 @@ import sys import uuid -import __main__ -from var import * -import controlflow -import display -import gapi.directory -import utils +import gam +from gam.var import * +from gam import controlflow +from gam import display +from gam import gapi +from gam.gapi import directory as gapi_directory +from gam import utils def printBuildings(): to_drive = False - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() titles = [] csvRows = [] fieldsList = ['buildingId'] @@ -65,7 +66,7 @@ def printBuildings(): def printResourceCalendars(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() todrive = False fieldsList = [] fieldsTitles = {} @@ -108,7 +109,7 @@ def printResourceCalendars(): if 'buildingId' in fieldsList: display.add_field_to_csv_file('buildingName', {'buildingName': [ 'buildingName', ]}, fieldsList, fieldsTitles, titles) - __main__.printGettingAllItems('Resource Calendars', None) + gam.printGettingAllItems('Resource Calendars', None) page_message = gapi.got_total_items_first_last_msg('Resource Calendars') resources = gapi.get_all_pages(cd.resources().calendars(), 'list', 'items', page_message=page_message, @@ -162,7 +163,7 @@ RESCAL_ARGUMENT_TO_PROPERTY_MAP = { def printFeatures(): to_drive = False - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() titles = [] csvRows = [] fieldsList = ['name'] @@ -240,7 +241,7 @@ def _getBuildingAttributes(args, body={}): def createBuilding(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() body = {'floorNames': ['1'], 'buildingId': str(uuid.uuid4()), 'buildingName': sys.argv[3]} @@ -318,7 +319,7 @@ def getBuildingNameById(cd, buildingId): def updateBuilding(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() buildingId = getBuildingByNameOrId(cd, sys.argv[3]) body = _getBuildingAttributes(sys.argv[4:]) print(f'Updating building {buildingId}...') @@ -328,7 +329,7 @@ def updateBuilding(): def getBuildingInfo(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() buildingId = getBuildingByNameOrId(cd, sys.argv[3]) building = gapi.call(cd.resources().buildings(), 'get', customer=GC_Values[GC_CUSTOMER_ID], @@ -343,7 +344,7 @@ def getBuildingInfo(): def deleteBuilding(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() buildingId = getBuildingByNameOrId(cd, sys.argv[3]) print(f'Deleting building {buildingId}...') gapi.call(cd.resources().buildings(), 'delete', @@ -364,7 +365,7 @@ def _getFeatureAttributes(args, body={}): def createFeature(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() body = _getFeatureAttributes(sys.argv[3:]) print(f'Creating feature {body["name"]}...') gapi.call(cd.resources().features(), 'insert', @@ -375,7 +376,7 @@ def updateFeature(): # update does not work for name and name is only field to be updated # if additional writable fields are added to feature in the future # we'll add support for update as well as rename - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() oldName = sys.argv[3] body = {'newName': sys.argv[5:]} print(f'Updating feature {oldName}...') @@ -385,7 +386,7 @@ def updateFeature(): def deleteFeature(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() featureKey = sys.argv[3] print(f'Deleting feature {featureKey}...') gapi.call(cd.resources().features(), 'delete', @@ -410,7 +411,7 @@ def _getResourceCalendarAttributes(cd, args, body={}): cd, args[i+1], minLen=0) i += 2 elif myarg in ['capacity']: - body['capacity'] = __main__.getInteger(args[i+1], myarg, minVal=0) + body['capacity'] = gam.getInteger(args[i+1], myarg, minVal=0) i += 2 elif myarg in ['feature', 'features']: features = args[i+1].split(',') @@ -440,7 +441,7 @@ def _getResourceCalendarAttributes(cd, args, body={}): def createResourceCalendar(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() body = {'resourceId': sys.argv[3], 'resourceName': sys.argv[4]} body = _getResourceCalendarAttributes(cd, sys.argv[5:], body) @@ -450,7 +451,7 @@ def createResourceCalendar(): def updateResourceCalendar(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() resId = sys.argv[3] body = _getResourceCalendarAttributes(cd, sys.argv[4:]) # Use patch since it seems to work better. @@ -462,7 +463,7 @@ def updateResourceCalendar(): def getResourceCalendarInfo(): - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() resId = sys.argv[3] resource = gapi.call(cd.resources().calendars(), 'get', customer=GC_Values[GC_CUSTOMER_ID], @@ -481,7 +482,7 @@ def getResourceCalendarInfo(): def deleteResourceCalendar(): resId = sys.argv[3] - cd = gapi.directory.buildGAPIObject() + cd = gapi_directory.buildGAPIObject() print(f'Deleting resource calendar {resId}') gapi.call(cd.resources().calendars(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], calendarResourceId=resId) diff --git a/src/gapi/errors.py b/src/gam/gapi/errors.py similarity index 98% rename from src/gapi/errors.py rename to src/gam/gapi/errors.py index ba9d8cd8..42c8f41e 100644 --- a/src/gapi/errors.py +++ b/src/gam/gapi/errors.py @@ -3,9 +3,9 @@ from enum import Enum import json -import controlflow -import display # TODO: Change to relative import when gam is setup as a package -from var import UTF8 +from gam import controlflow +from gam import display +from gam.var import UTF8 class GapiAbortedError(Exception): diff --git a/src/gapi/errors_test.py b/src/gam/gapi/errors_test.py similarity index 99% rename from src/gapi/errors_test.py rename to src/gam/gapi/errors_test.py index 4ec0c682..82a0bca2 100644 --- a/src/gapi/errors_test.py +++ b/src/gam/gapi/errors_test.py @@ -6,7 +6,7 @@ import unittest from unittest.mock import patch import googleapiclient.errors -from gapi import errors +from gam.gapi import errors def create_simple_http_error(status, reason, message): diff --git a/src/gapi/reports.py b/src/gam/gapi/reports.py similarity index 97% rename from src/gapi/reports.py rename to src/gam/gapi/reports.py index fc189907..5abc01cc 100644 --- a/src/gapi/reports.py +++ b/src/gam/gapi/reports.py @@ -5,16 +5,16 @@ import sys from dateutil.relativedelta import relativedelta -import __main__ -from var import * -import controlflow -import display -import gapi -import utils +import gam +from gam.var import * +from gam import controlflow +from gam import display +from gam import gapi +from gam import utils def buildGAPIObject(): - return __main__.buildGAPIObject('reports') + return gam.buildGAPIObject('reports') REPORT_CHOICE_MAP = { @@ -55,7 +55,7 @@ def showUsageParameters(): kwargs = {} elif report == 'user': endpoint = rep.userUsageReport() - kwargs = {'userKey': __main__._getValueFromOAuth('email')} + kwargs = {'userKey': gam._getValueFromOAuth('email')} else: controlflow.expected_argument_exit( 'usageparameters', ['user', 'customer'], report) @@ -170,10 +170,10 @@ def showUsage(): skip_day_numbers = [dow.index(d) for d in skipdaynames if d in dow] i += 2 elif report == 'user' and myarg in ['orgunit', 'org', 'ou']: - _, orgUnitId = __main__.getOrgUnitId(sys.argv[i+1]) + _, orgUnitId = gam.getOrgUnitId(sys.argv[i+1]) i += 2 elif report == 'user' and myarg in usergroup_types: - users = __main__.getUsersToModify(myarg, sys.argv[i+1]) + users = gam.getUsersToModify(myarg, sys.argv[i+1]) kwargs = [{'userKey': user} for user in users] i += 2 else: @@ -286,7 +286,7 @@ def showReport(): tryDate = utils.get_yyyymmdd(sys.argv[i+1]) i += 2 elif myarg in ['orgunit', 'org', 'ou']: - _, orgUnitId = __main__.getOrgUnitId(sys.argv[i+1]) + _, orgUnitId = gam.getOrgUnitId(sys.argv[i+1]) i += 2 elif myarg == 'fulldatarequired': fullDataRequired = [] @@ -304,7 +304,7 @@ def showReport(): eventName = sys.argv[i+1] i += 2 elif myarg == 'user': - userKey = __main__.normalizeEmailAddressOrUID(sys.argv[i+1]) + userKey = gam.normalizeEmailAddressOrUID(sys.argv[i+1]) i += 2 elif myarg in ['filter', 'filters']: filters = sys.argv[i+1] diff --git a/src/gapi/storage.py b/src/gam/gapi/storage.py similarity index 94% rename from src/gapi/storage.py rename to src/gam/gapi/storage.py index 546c7958..af68738e 100644 --- a/src/gapi/storage.py +++ b/src/gam/gapi/storage.py @@ -5,16 +5,15 @@ import sys import googleapiclient -import __main__ -from var import * -import controlflow -import fileutils -import gapi -import utils +import gam +from gam.var import * +from gam import fileutils +from gam import gapi +from gam import utils def build_gapi(): - return __main__.buildGAPIObject('storage') + return gam.buildGAPIObject('storage') def get_cloud_storage_object(s, bucket, object_, local_file=None, diff --git a/src/gapi/vault.py b/src/gam/gapi/vault.py similarity index 94% rename from src/gapi/vault.py rename to src/gam/gapi/vault.py index fb89a954..26185a77 100644 --- a/src/gapi/vault.py +++ b/src/gam/gapi/vault.py @@ -4,24 +4,24 @@ import sys import googleapiclient.http -import __main__ -from var import * -import controlflow -import display -import fileutils -import gapi -import gapi.storage -import utils +import gam +from gam.var import * +from gam import controlflow +from gam import display +from gam import fileutils +from gam import gapi +from gam.gapi import storage as gapi_storage +from gam import utils def buildGAPIObject(): - return __main__.buildGAPIObject('vault') + return gam.buildGAPIObject('vault') def validateCollaborators(collaboratorList, cd): collaborators = [] for collaborator in collaboratorList.split(','): - collaborator_id = __main__.convertEmailAddressToUID(collaborator, cd) + collaborator_id = gam.convertEmailAddressToUID(collaborator, cd) if not collaborator_id: controlflow.system_error_exit(4, f'failed to get a UID for ' f'{collaborator}. Please make ' @@ -47,7 +47,7 @@ def createMatter(): i += 2 elif myarg in ['collaborator', 'collaborators']: if not cd: - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') collaborators.extend(validateCollaborators(sys.argv[i+1], cd)) i += 2 else: @@ -124,7 +124,7 @@ def createExport(): i += 2 elif searchMethod == 'ORG_UNIT': body['query']['orgUnitInfo'] = { - 'orgUnitId': __main__.getOrgUnitId(sys.argv[i+1])[1]} + 'orgUnitId': gam.getOrgUnitId(sys.argv[i+1])[1]} i += 2 elif searchMethod == 'SHARED_DRIVE': body['query']['sharedDriveInfo'] = { @@ -158,7 +158,7 @@ def createExport(): i += 2 elif myarg in ['excludedrafts']: body['query']['mailOptions'] = { - 'excludeDrafts': __main__.getBoolean(sys.argv[i+1], myarg)} + 'excludeDrafts': gam.getBoolean(sys.argv[i+1], myarg)} i += 2 elif myarg in ['driveversiondate']: body['query'].setdefault('driveOptions', {})['versionDate'] = \ @@ -166,11 +166,11 @@ def createExport(): i += 2 elif myarg in ['includeshareddrives', 'includeteamdrives']: body['query'].setdefault('driveOptions', {})[ - 'includeSharedDrives'] = __main__.getBoolean(sys.argv[i+1], myarg) + 'includeSharedDrives'] = gam.getBoolean(sys.argv[i+1], myarg) i += 2 elif myarg in ['includerooms']: body['query']['hangoutsChatOptions'] = { - 'includeRooms': __main__.getBoolean(sys.argv[i+1], myarg)} + 'includeRooms': gam.getBoolean(sys.argv[i+1], myarg)} i += 2 elif myarg in ['format']: export_format = sys.argv[i+1].upper() @@ -179,7 +179,7 @@ def createExport(): "export format", ", ".join(allowed_formats), export_format) i += 2 elif myarg in ['showconfidentialmodecontent']: - showConfidentialModeContent = __main__.getBoolean(sys.argv[i+1], myarg) + showConfidentialModeContent = gam.getBoolean(sys.argv[i+1], myarg) i += 2 elif myarg in ['region']: allowed_regions = gapi.get_enum_values_minus_unspecified( @@ -192,7 +192,7 @@ def createExport(): i += 2 elif myarg in ['includeaccessinfo']: body['exportOptions'].setdefault('driveOptions', {})[ - 'includeAccessInfo'] = __main__.getBoolean(sys.argv[i+1], myarg) + 'includeAccessInfo'] = gam.getBoolean(sys.argv[i+1], myarg) i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], "gam create export") @@ -277,7 +277,7 @@ def createHold(): i += 2 elif myarg in ['orgunit', 'ou']: body['orgUnit'] = { - 'orgUnitId': __main__.getOrgUnitId(sys.argv[i+1])[1]} + 'orgUnitId': gam.getOrgUnitId(sys.argv[i+1])[1]} i += 2 elif myarg in ['start', 'starttime']: start_time = utils.get_date_zero_time_or_full_time(sys.argv[i+1]) @@ -319,11 +319,11 @@ def createHold(): body['query'][query_type]['endTime'] = end_time if accounts: body['accounts'] = [] - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') account_type = 'group' if body['corpus'] == 'GROUPS' else 'user' for account in accounts: body['accounts'].append( - {'accountId': __main__.convertEmailAddressToUID(account, + {'accountId': gam.convertEmailAddressToUID(account, cd, account_type)} ) @@ -370,16 +370,16 @@ def getHoldInfo(): 3, 'you must specify a matter for the hold.') results = gapi.call(v.matters().holds(), 'get', matterId=matterId, holdId=holdId) - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') if 'accounts' in results: account_type = 'group' if results['corpus'] == 'GROUPS' else 'user' for i in range(0, len(results['accounts'])): uid = f'uid:{results["accounts"][i]["accountId"]}' - acct_email = __main__.convertUIDtoEmailAddress( + acct_email = gam.convertUIDtoEmailAddress( uid, cd, [account_type]) results['accounts'][i]['email'] = acct_email if 'orgUnit' in results: - results['orgUnit']['orgUnitPath'] = __main__.doGetOrgInfo( + results['orgUnit']['orgUnitPath'] = gam.doGetOrgInfo( results['orgUnit']['orgUnitId'], return_attrib='orgUnitPath') display.print_json(results) @@ -456,7 +456,7 @@ def updateHold(): query = sys.argv[i+1] i += 2 elif myarg in ['orgunit', 'ou']: - body['orgUnit'] = {'orgUnitId': __main__.getOrgUnitId(sys.argv[i+1])[1]} + body['orgUnit'] = {'orgUnitId': gam.getOrgUnitId(sys.argv[i+1])[1]} i += 2 elif myarg in ['start', 'starttime']: start_time = utils.get_date_zero_time_or_full_time(sys.argv[i+1]) @@ -505,15 +505,15 @@ def updateHold(): gapi.call(v.matters().holds(), 'update', matterId=matterId, holdId=holdId, body=body) if add_accounts or del_accounts: - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') for account in add_accounts: print(f'adding {account} to hold.') - add_body = {'accountId': __main__.convertEmailAddressToUID(account, cd)} + add_body = {'accountId': gam.convertEmailAddressToUID(account, cd)} gapi.call(v.matters().holds().accounts(), 'create', matterId=matterId, holdId=holdId, body=add_body) for account in del_accounts: print(f'removing {account} from hold.') - accountId = __main__.convertEmailAddressToUID(account, cd) + accountId = gam.convertEmailAddressToUID(account, cd) gapi.call(v.matters().holds().accounts(), 'delete', matterId=matterId, holdId=holdId, accountId=accountId) @@ -543,12 +543,12 @@ def updateMatter(action=None): i += 2 elif myarg in ['addcollaborator', 'addcollaborators']: if not cd: - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') add_collaborators.extend(validateCollaborators(sys.argv[i+1], cd)) i += 2 elif myarg in ['removecollaborator', 'removecollaborators']: if not cd: - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') remove_collaborators.extend( validateCollaborators(sys.argv[i+1], cd)) i += 2 @@ -585,10 +585,10 @@ def getMatterInfo(): matterId = getMatterItem(v, sys.argv[3]) result = gapi.call(v.matters(), 'get', matterId=matterId, view='FULL') if 'matterPermissions' in result: - cd = __main__.buildGAPIObject('directory') + cd = gam.buildGAPIObject('directory') for i in range(0, len(result['matterPermissions'])): uid = f'uid:{result["matterPermissions"][i]["accountId"]}' - user_email = __main__.convertUIDtoEmailAddress(uid, cd) + user_email = gam.convertUIDtoEmailAddress(uid, cd) result['matterPermissions'][i]['email'] = user_email display.print_json(result) @@ -597,7 +597,7 @@ def downloadExport(): verifyFiles = True extractFiles = True v = buildGAPIObject() - s = gapi.storage.build_gapi() + s = gapi_storage.build_gapi() matterId = getMatterItem(v, sys.argv[3]) exportId = convertExportNameToID(v, sys.argv[4], matterId) targetFolder = GC_Values[GC_DRIVE_DIR] @@ -643,7 +643,7 @@ def downloadExport(): utils.md5_matches_file(filename, expected_hash, True) print('VERIFIED') if extractFiles and re.search(r'\.zip$', filename): - __main__.extract_nested_zip(filename, targetFolder) + gam.extract_nested_zip(filename, targetFolder) def printMatters(): @@ -674,7 +674,7 @@ def printMatters(): i += 2 else: controlflow.invalid_argument_exit(myarg, "gam print matters") - __main__.printGettingAllItems('Vault Matters', None) + gam.printGettingAllItems('Vault Matters', None) page_message = gapi.got_total_items_msg('Vault Matters', '...\n') matters = gapi.get_all_pages( v.matters(), 'list', 'matters', page_message=page_message, view=view, diff --git a/src/transport.py b/src/gam/transport.py similarity index 94% rename from src/transport.py rename to src/gam/transport.py index 59042c60..6d13a939 100644 --- a/src/transport.py +++ b/src/gam/transport.py @@ -3,11 +3,11 @@ import google_auth_httplib2 import httplib2 -from var import GAM_INFO -from var import GC_CA_FILE -from var import GC_TLS_MAX_VERSION -from var import GC_TLS_MIN_VERSION -from var import GC_Values +from gam.var import GAM_INFO +from gam.var import GC_CA_FILE +from gam.var import GC_TLS_MAX_VERSION +from gam.var import GC_TLS_MIN_VERSION +from gam.var import GC_Values def create_http(cache=None, diff --git a/src/transport_test.py b/src/gam/transport_test.py similarity index 99% rename from src/transport_test.py rename to src/gam/transport_test.py index f6374ab9..077ccb4d 100644 --- a/src/transport_test.py +++ b/src/gam/transport_test.py @@ -8,7 +8,7 @@ from gam import SetGlobalVariables import google_auth_httplib2 import httplib2 -import transport +from gam import transport class CreateHttpTest(unittest.TestCase): diff --git a/src/utils.py b/src/gam/utils.py similarity index 98% rename from src/utils.py rename to src/gam/utils.py index fead8e6a..47f02bf0 100644 --- a/src/utils.py +++ b/src/gam/utils.py @@ -8,10 +8,10 @@ from html.parser import HTMLParser import json import dateutil.parser -import controlflow -import fileutils -import transport -from var import * +from gam import controlflow +from gam import fileutils +from gam import transport +from gam.var import * class _DeHTMLParser(HTMLParser): diff --git a/src/var.py b/src/gam/var.py similarity index 100% rename from src/var.py rename to src/gam/var.py diff --git a/src/gapi/directory/__init__.py b/src/gapi/directory/__init__.py deleted file mode 100644 index 68e2d0cf..00000000 --- a/src/gapi/directory/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import __main__ - - -def buildGAPIObject(): - return __main__.buildGAPIObject('directory') diff --git a/src/travis/linux-install.sh b/src/travis/linux-install.sh index df539fb9..f68ae971 100755 --- a/src/travis/linux-install.sh +++ b/src/travis/linux-install.sh @@ -1,11 +1,11 @@ cd src if [[ "$TRAVIS_JOB_NAME" == *"Testing" ]]; then - export gam="$python gam.py" + export gam="$python -m gam" export gampath=$(readlink -e .) else - $python -OO -m PyInstaller --clean --noupx --strip -F --distpath=gam gam.spec - export gam="gam/gam" + $python -OO -m PyInstaller --clean --noupx --strip -F gam.spec export gampath=$(readlink -e gam) + export gam="${gampath}/gam" export GAMVERSION=`$gam version simple` cp LICENSE $gampath cp whatsnew.txt $gampath @@ -13,21 +13,21 @@ else this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}') GAM_ARCHIVE=gam-$GAMVERSION-$GAMOS-$PLATFORM-glibc$this_glibc_ver.tar.xz rm $gampath/lastupdatecheck.txt - tar cfJ $GAM_ARCHIVE gam/ + tar cfJ --transform s/dist/gam/ $GAM_ARCHIVE $gampath echo "PyInstaller GAM info:" - du -h gam/gam + du -h $gam time $gam version extended - if ([ "${TRAVIS_DIST}" == "trusty" ] || [ "${TRAVIS_DIST}" == "xenial" ]) && [ "${PLATFORM}" == "x86_64" ]; then + if [ "${TRAVIS_DIST}" == "xenial" ] && [ "${PLATFORM}" == "x86_64" ]; then GAM_LEGACY_ARCHIVE=gam-${GAMVERSION}-${GAMOS}-${PLATFORM}-legacy.tar.xz - $python -OO -m staticx -l /lib/x86_64-linux-gnu/libresolv.so.2 -l /lib/x86_64-linux-gnu/libnss_dns.so.2 gam/gam gam/gam-staticx - strip gam/gam-staticx - rm gam/gam - mv gam/gam-staticx gam/gam - chmod 755 gam/gam - tar cvfJ $GAM_LEGACY_ARCHIVE gam/ + $python -OO -m staticx -l /lib/x86_64-linux-gnu/libresolv.so.2 -l /lib/x86_64-linux-gnu/libnss_dns.so.2 $gam $gam-staticx + strip $gam-staticx + rm $gampath/gam + mv $gam-staticx $gam + chmod 755 $gam + tar cvfJ --transform s/dist/gam/ $GAM_LEGACY_ARCHIVE $gampath echo "Legacy StaticX GAM info:" - du -h gam/gam + du -h $gam time $gam version extended fi echo "GAM packages:" diff --git a/src/travis/osx-install.sh b/src/travis/osx-install.sh index 37d95c61..81d43434 100755 --- a/src/travis/osx-install.sh +++ b/src/travis/osx-install.sh @@ -1,15 +1,15 @@ cd src echo "MacOS Version Info According to Python:" python -c "import platform; print(platform.mac_ver())" -$python -OO -m PyInstaller --clean --noupx --strip -F --distpath=gam gam.spec -export gam="gam/gam" -export gampath=gam +$python -OO -m PyInstaller --clean --noupx --strip -F gam.spec +export gampath=dist +export gam="$gampath/gam" $gam version extended -export GAMVERSION=`gam/gam version simple` -cp LICENSE gam -cp whatsnew.txt gam -cp GamCommands.txt gam +export GAMVERSION=`$gam version simple` +cp LICENSE $gampath +cp whatsnew.txt $gampath +cp GamCommands.txt $gampath MACOSVERSION=$(defaults read loginwindow SystemVersionStampAsString) GAM_ARCHIVE=gam-$GAMVERSION-$GAMOS-$PLATFORM-MacOS$MACOSVERSION.tar.xz -rm gam/lastupdatecheck.txt -tar cfJ $GAM_ARCHIVE gam/ +rm $gampath/lastupdatecheck.txt +tar cfJ $GAM_ARCHIVE --transform s/$gampath/gam $gampath diff --git a/src/travis/windows-install.sh b/src/travis/windows-install.sh index c4e2a06e..348b0de9 100755 --- a/src/travis/windows-install.sh +++ b/src/travis/windows-install.sh @@ -1,20 +1,21 @@ cd src echo "compiling GAM with pyinstaller..." -pyinstaller --clean --noupx -F --distpath=gam gam.spec -export gam="gam/gam" +pyinstaller --clean --noupx -F gam.spec export gampath=$(readlink -e gam) +export gam="${gampath}/gam" echo "running compiled GAM..." $gam version export GAMVERSION=`$gam version simple` -rm gam/lastupdatecheck.txt -cp LICENSE gam -cp GamCommands.txt gam -cp whatsnew.txt gam -cp gam-setup.bat gam +rm $gampath/lastupdatecheck.txt +cp LICENSE $gampath +cp GamCommands.txt $gampath +cp whatsnew.txt $gampath +cp gam-setup.bat $gampath GAM_ARCHIVE=gam-$GAMVERSION-$GAMOS-$PLATFORM.zip -/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE gam -xr!.svn +/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE $gampath -xr!.svn +/c/Program\ Files/7-Zip/7z.exe rn $GAM_ARCHIVE dist\ gam\ mkdir gam-64 -cp -rf gam/* gam-64/; +cp -rf $gampath/* gam-64/ /c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.11/bin/candle.exe -arch $WIX_BITS gam.wxs /c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.11/bin/light.exe -ext /c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.11/bin/WixUIExtension.dll gam.wixobj -o gam-$GAMVERSION-$GAMOS-$PLATFORM.msi || true; rm *.wixpdb