mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-21 22:51:37 +00:00
11721 lines
454 KiB
Python
Executable File
11721 lines
454 KiB
Python
Executable File
#!/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 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 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.id_token
|
|
import google.oauth2.service_account
|
|
import google_auth_oauthlib.flow
|
|
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
|
|
|
|
from filelock import FileLock
|
|
|
|
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'])
|
|
# Pyinstaller executable
|
|
elif getattr(sys, 'frozen', False):
|
|
GM_Globals[GM_GAM_PATH] = os.path.dirname(sys.executable)
|
|
else:
|
|
# Source code
|
|
GM_Globals[GM_GAM_PATH] = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
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 integerLimits(minVal, maxVal, item='integer'):
|
|
if (minVal is not None) and (maxVal is not None):
|
|
return f'{item} {minVal}<=x<={maxVal}'
|
|
if minVal is not None:
|
|
return f'{item} x>={minVal}'
|
|
if maxVal is not None:
|
|
return f'{item} x<={maxVal}'
|
|
return f'{item} x'
|
|
|
|
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 <{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: <Number>')
|
|
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<Operator><Value>) or (boolean:true|false) or (regex|notregex:<RegularExpression>)')
|
|
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_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
|
|
print((f'GAM {gam_version} - {GAM_URL}\n'
|
|
f'{gam_author}\n'
|
|
f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n'
|
|
f'google-api-python-client {googleapiclient.__version__}\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'
|
|
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():
|
|
oauth_string = fileutils.read_file(GC_Values[GC_OAUTH2_TXT], continue_on_error=True, display_errors=False)
|
|
if not oauth_string:
|
|
return None
|
|
oauth_data = json.loads(oauth_string)
|
|
creds = google.oauth2.credentials.Credentials.from_authorized_user_file(GC_Values[GC_OAUTH2_TXT])
|
|
creds.token = oauth_data.get('token', oauth_data.get('auth_token', ''))
|
|
creds._id_token = oauth_data.get('id_token_jwt', oauth_data.get('id_token', None))
|
|
token_expiry = oauth_data.get('token_expiry', '1970-01-01T00:00:01Z')
|
|
creds.expiry = datetime.datetime.strptime(token_expiry, '%Y-%m-%dT%H:%M:%SZ')
|
|
GC_Values[GC_DECODED_ID_TOKEN] = oauth_data.get('decoded_id_token', '')
|
|
return creds
|
|
|
|
def getValidOauth2TxtCredentials(force_refresh=False):
|
|
"""Gets OAuth2 credentials which are guaranteed to be fresh and valid.
|
|
Locks during read and possible write so that only one process will
|
|
attempt refresh/write when running in parallel. """
|
|
lock_file = f'{GC_Values[GC_OAUTH2_TXT]}.lock'
|
|
lock = FileLock(lock_file)
|
|
with lock:
|
|
credentials = getOauth2TxtStorageCredentials()
|
|
if (credentials and credentials.expired) or force_refresh:
|
|
retries = 3
|
|
for n in range(1, retries+1):
|
|
try:
|
|
credentials.refresh(transport.create_request())
|
|
writeCredentials(credentials)
|
|
break
|
|
except google.auth.exceptions.RefreshError as e:
|
|
try:
|
|
if e.args[0] in REFRESH_PERM_ERRORS:
|
|
# remove OAuth file so we kick off auth next time
|
|
os.remove(GC_Values[GC_OAUTH2_TXT])
|
|
except SyntaxError:
|
|
pass
|
|
controlflow.system_error_exit(18, str(e))
|
|
except (google.auth.exceptions.TransportError, httplib2.ServerNotFoundError, RuntimeError) as e:
|
|
if n != retries:
|
|
controlflow.wait_on_failure(n, retries, str(e))
|
|
continue
|
|
controlflow.system_error_exit(4, str(e))
|
|
elif credentials is None or not credentials.valid:
|
|
doRequestOAuth()
|
|
credentials = getOauth2TxtStorageCredentials()
|
|
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) 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 <email> 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 = googleapiclient.discovery.build('oauth2', 'v1', 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 = googleapiclient.discovery.build('oauth2', 'v1', 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.')
|
|
return
|
|
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 = 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 <users> 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 <users> 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 <UserItem>)')
|
|
if 'name' not in body:
|
|
controlflow.system_error_exit(2, 'expected name <String>)')
|
|
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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> {['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 = 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
|
|
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)
|
|
titleInfo = f'{result["title"]}({result["id"]})'
|
|
if csv_output:
|
|
csv_rows.append({'User': user, 'title': result['title'], 'id': result['id']})
|
|
else:
|
|
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 <users> 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 <users> 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 <users> 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 <users> {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 <users> 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 <users> 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 <users> imap maxfoldersize", "| ".join(EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES), opt)
|
|
else:
|
|
controlflow.invalid_argument_exit(myarg, "gam <users> 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 <users> 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 <users> 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 <users> pop action", ", ".join(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICES_MAP), opt)
|
|
elif myarg == 'confirm':
|
|
i += 1
|
|
else:
|
|
controlflow.invalid_argument_exit(myarg, "gam <users> 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', '<br/>')
|
|
if tagReplacements:
|
|
signature = _processTags(tagReplacements, signature)
|
|
if not html:
|
|
signature = signature.replace('\n', '<br/>')
|
|
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 <users> {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 <users> {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 <users> 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 <users> 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 <users> {['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 <users> {['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 <users> 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 <users> 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 <users> {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 <LabelColorHex> is required.')
|
|
controlflow.system_error_exit(2, 'backgroundcolor <LabelColorHex> 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 <users> {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 <users> 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 <users> 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 <users> 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 <users> filter")
|
|
if 'criteria' not in body:
|
|
controlflow.system_error_exit(2, f'you must specify a crtieria <{"|".join(FILTER_CRITERIA_CHOICES_MAP)}> for "gam <users> filter"')
|
|
if 'action' not in body:
|
|
controlflow.system_error_exit(2, f'you must specify an action <{"|".join(FILTER_ACTION_CHOICES)}> for "gam <users> 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 <users> {['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 <users> 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 <users> 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 <users> 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 <users> {['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 <users> {['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 <users> 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 <users> 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 <users> vacation")
|
|
if message:
|
|
if responseBodyType == 'responseBodyHtml':
|
|
message = message.replace('\r', '').replace('\\n', '<br/>')
|
|
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 <users> 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 <users> {['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|<String>, a|b|c|custom <String>
|
|
setTypeCustom=True, For all fields except organizations, when setting a custom type you do:
|
|
entry[u'type'] = u'custom'
|
|
entry[u'customType'] = <String>
|
|
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 shorten_url(long_url):
|
|
simplehttp = transport.create_http(timeout=10)
|
|
url_shortnr = 'https://gam-shortn.appspot.com/create'
|
|
headers = {'Content-Type': 'application/json',
|
|
'User-Agent': GAM_INFO}
|
|
try:
|
|
resp, content = simplehttp.request(url_shortnr, 'POST',
|
|
f'{{"long_url": "{long_url}"}}', headers=headers)
|
|
except Exception:
|
|
return long_url
|
|
if resp.status != 200:
|
|
return long_url
|
|
try:
|
|
return json.loads(content).get('short_url', long_url)
|
|
except Exception:
|
|
print(content)
|
|
return long_url
|
|
|
|
class ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
|
def authorization_url(self, **kwargs):
|
|
long_url, state = super(ShortURLFlow, self).authorization_url(**kwargs)
|
|
short_url = shorten_url(long_url)
|
|
return short_url, state
|
|
|
|
def _run_oauth_flow(client_id, client_secret, scopes, access_type, login_hint=None):
|
|
client_config = {
|
|
'installed': {
|
|
'client_id': client_id,
|
|
'client_secret': client_secret,
|
|
'redirect_uris': ['http://localhost', 'urn:ietf:wg:oauth:2.0:oob'],
|
|
'auth_uri': 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
'token_uri': 'https://oauth2.googleapis.com/token',
|
|
}
|
|
}
|
|
|
|
flow = ShortURLFlow.from_client_config(client_config, scopes, autogenerate_code_verifier=True)
|
|
kwargs = {'access_type': access_type}
|
|
if login_hint:
|
|
kwargs['login_hint'] = login_hint
|
|
if not GC_Values[GC_OAUTH_BROWSER]:
|
|
flow.run_console(
|
|
authorization_prompt_message=MESSAGE_CONSOLE_AUTHORIZATION_PROMPT,
|
|
authorization_code_message=MESSAGE_CONSOLE_AUTHORIZATION_CODE,
|
|
**kwargs)
|
|
else:
|
|
flow.run_local_server(
|
|
authorization_prompt_message=MESSAGE_LOCAL_SERVER_AUTHORIZATION_PROMPT,
|
|
success_message=MESSAGE_LOCAL_SERVER_SUCCESS,
|
|
**kwargs)
|
|
return flow.credentials
|
|
|
|
def getCRMService(login_hint):
|
|
scopes = ['https://www.googleapis.com/auth/cloud-platform']
|
|
client_id = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com'
|
|
client_secret = 'qM3dP8f_4qedwzWQE1VR4zzU'
|
|
credentials = _run_oauth_flow(client_id, client_secret, scopes, 'online', login_hint)
|
|
httpc = transport.AuthorizedHttp(credentials)
|
|
return (googleapiclient.discovery.build('cloudresourcemanager', 'v1',
|
|
http=httpc, cache_discovery=False,
|
|
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI),
|
|
httpc)
|
|
|
|
# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here.
|
|
def getCRM2Service(httpc):
|
|
return googleapiclient.discovery.build('cloudresourcemanager', 'v2',
|
|
http=httpc, cache_discovery=False,
|
|
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI)
|
|
|
|
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 = googleapiclient.discovery.build('servicemanagement', 'v1',
|
|
http=httpObj, cache_discovery=False,
|
|
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI)
|
|
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 = googleapiclient.discovery.build('iap', 'v1',
|
|
http=httpObj, cache_discovery=False,
|
|
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI)
|
|
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 = googleapiclient.discovery.build('iam', 'v1',
|
|
http=httpObj, cache_discovery=False,
|
|
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI)
|
|
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" 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 <email> <project-id>
|
|
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|<ProjectID>|(filter <String>)'
|
|
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 = googleapiclient.discovery.build('iam', 'v1',
|
|
http=httpObj, cache_discovery=False,
|
|
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI)
|
|
_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)
|
|
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):
|
|
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/{quote(client_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 <users> {['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 <users> 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 <users> 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 <users> 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 <users> 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'] = '<Unknown Team Drive>'
|
|
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 GC_Values[GC_DECODED_ID_TOKEN]:
|
|
credentials = credentials if credentials is not None else getValidOauth2TxtCredentials()
|
|
request = transport.create_request()
|
|
GC_Values[GC_DECODED_ID_TOKEN] = google.oauth2.id_token.verify_oauth2_token(credentials.id_token, request)
|
|
return GC_Values[GC_DECODED_ID_TOKEN].get(field, 'Unknown')
|
|
|
|
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 <users> delete token")
|
|
if not clientId:
|
|
controlflow.system_error_exit(3, 'you must specify a clientid for "gam <users> 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 <users> {['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:<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)
|
|
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
|
|
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, '<None>'))
|
|
appDetails.append(str(app.get('versionCode', '<None>')))
|
|
permissions = app.get('permission', [])
|
|
if permissions:
|
|
appDetails.append('/'.join(permissions))
|
|
else:
|
|
appDetails.append('<None>')
|
|
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():
|
|
lock_file = f'{GC_Values[GC_OAUTH2_TXT]}.lock'
|
|
lock = FileLock(lock_file, timeout=10)
|
|
with lock:
|
|
credentials = getOauth2TxtStorageCredentials()
|
|
if credentials is None:
|
|
return
|
|
simplehttp = transport.create_http()
|
|
params = {'token': credentials.refresh_token}
|
|
revoke_uri = f'https://accounts.google.com/o/oauth2/revoke?{urlencode(params)}'
|
|
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()
|
|
simplehttp.request(revoke_uri, 'GET')
|
|
os.remove(GC_Values[GC_OAUTH2_TXT])
|
|
if not GM_Globals[GM_WINDOWS]:
|
|
try:
|
|
os.remove(lock_file)
|
|
except IOError:
|
|
pass
|
|
|
|
def writeCredentials(creds):
|
|
creds_data = {
|
|
'token': creds.token,
|
|
'refresh_token': creds.refresh_token,
|
|
'token_uri': creds.token_uri,
|
|
'client_id': creds.client_id,
|
|
'client_secret': creds.client_secret,
|
|
'id_token': creds.id_token,
|
|
'token_expiry': creds.expiry.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
#'scopes': sorted(creds.scopes), # Google auth doesn't currently give us scopes back on refresh
|
|
}
|
|
expected_iss = ['https://accounts.google.com', 'accounts.google.com']
|
|
if _getValueFromOAuth('iss', creds) not in expected_iss:
|
|
controlflow.system_error_exit(13, f'Wrong OAuth 2.0 credentials issuer. Got {_getValueFromOAuth("iss", creds)} expected one of {", ".join(expected_iss)}')
|
|
request = transport.create_request()
|
|
creds_data['decoded_id_token'] = google.oauth2.id_token.verify_oauth2_token(creds.id_token, request)
|
|
data = json.dumps(creds_data, indent=2, sort_keys=True)
|
|
fileutils.write_file(GC_Values[GC_OAUTH2_TXT], data)
|
|
|
|
def doRequestOAuth(login_hint=None):
|
|
credentials = getOauth2TxtStorageCredentials()
|
|
if credentials is None or not credentials.valid:
|
|
scopes = getScopesFromUser()
|
|
if scopes is None:
|
|
controlflow.system_error_exit(0, '')
|
|
client_id, client_secret = getOAuthClientIDAndSecret()
|
|
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'
|
|
creds = _run_oauth_flow(client_id, client_secret, scopes, 'offline', login_hint)
|
|
writeCredentials(creds)
|
|
else:
|
|
print(f'It looks like you\'ve already authorized GAM. Refusing to overwrite existing file:\n\n{GC_Values[GC_OAUTH2_TXT]}')
|
|
|
|
def getOAuthClientIDAndSecret():
|
|
"""Retrieves the OAuth client ID and client secret from JSON."""
|
|
MISSING_CLIENT_SECRETS_MESSAGE = '''To use GAM you need to create an API project. Please run:
|
|
|
|
gam create project
|
|
'''
|
|
filename = GC_Values[GC_CLIENT_SECRETS_JSON]
|
|
cs_data = fileutils.read_file(filename, continue_on_error=True, display_errors=True)
|
|
if not cs_data:
|
|
controlflow.system_error_exit(14, MISSING_CLIENT_SECRETS_MESSAGE)
|
|
try:
|
|
cs_json = json.loads(cs_data)
|
|
client_id = cs_json['installed']['client_id']
|
|
# chop off .apps.googleusercontent.com suffix as it's not needed
|
|
# and we need to keep things short for the Auth URL.
|
|
client_id = re.sub(r'\.apps\.googleusercontent\.com$', '', client_id)
|
|
client_secret = cs_json['installed']['client_secret']
|
|
except (ValueError, IndexError, KeyError):
|
|
controlflow.system_error_exit(3, f'the format of your client secrets file:\n\n{filename}\n\n'
|
|
'is incorrect. Please recreate the file.')
|
|
return (client_id, client_secret)
|
|
|
|
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 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:
|
|
|
|
[<*>] <##>) <Feature description> (<supported scope modifiers>) [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<scope_number>\d{1,2})\s*(?P<restriction>[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 <gam|commit-batch>\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 <filename>" 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> 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 <users> {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 <users> {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 <users> 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 <users> get")
|
|
elif command == 'empty':
|
|
emptyWhat = sys.argv[4].lower()
|
|
if emptyWhat == 'drivetrash':
|
|
doEmptyDriveTrash(users)
|
|
else:
|
|
controlflow.invalid_argument_exit(emptyWhat, "gam <users> 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 <users> info")
|
|
elif command == 'check':
|
|
checkWhat = sys.argv[4].replace('_', '').lower()
|
|
if checkWhat == 'serviceaccount':
|
|
doCheckServiceAccount(users)
|
|
else:
|
|
controlflow.invalid_argument_exit(checkWhat, "gam <users> 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 <users> 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]
|
|
|
|
# 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))
|